@mastra/s3 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,584 @@
1
+ import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, CopyObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, HeadObjectCommand, HeadBucketCommand } from '@aws-sdk/client-s3';
2
+ import { MastraFilesystem, FileNotFoundError } from '@mastra/core/workspace';
3
+
4
+ // src/filesystem/index.ts
5
+ var MIME_TYPES = {
6
+ // Text
7
+ ".txt": "text/plain",
8
+ ".md": "text/markdown",
9
+ ".markdown": "text/markdown",
10
+ ".html": "text/html",
11
+ ".htm": "text/html",
12
+ ".css": "text/css",
13
+ ".csv": "text/csv",
14
+ ".xml": "text/xml",
15
+ // Code
16
+ ".js": "text/javascript",
17
+ ".mjs": "text/javascript",
18
+ ".ts": "text/typescript",
19
+ ".tsx": "text/typescript",
20
+ ".jsx": "text/javascript",
21
+ ".json": "application/json",
22
+ ".yaml": "text/yaml",
23
+ ".yml": "text/yaml",
24
+ ".py": "text/x-python",
25
+ ".rb": "text/x-ruby",
26
+ ".sh": "text/x-shellscript",
27
+ ".bash": "text/x-shellscript",
28
+ // Images
29
+ ".png": "image/png",
30
+ ".jpg": "image/jpeg",
31
+ ".jpeg": "image/jpeg",
32
+ ".gif": "image/gif",
33
+ ".svg": "image/svg+xml",
34
+ ".webp": "image/webp",
35
+ ".ico": "image/x-icon",
36
+ // Documents
37
+ ".pdf": "application/pdf",
38
+ // Archives
39
+ ".zip": "application/zip",
40
+ ".gz": "application/gzip",
41
+ ".tar": "application/x-tar"
42
+ };
43
+ function getMimeType(path) {
44
+ const ext = path.toLowerCase().match(/\.[^.]+$/)?.[0];
45
+ return ext ? MIME_TYPES[ext] ?? "application/octet-stream" : "application/octet-stream";
46
+ }
47
+ function isNotFoundError(error) {
48
+ if (!error || typeof error !== "object" || !("name" in error)) return false;
49
+ const name = error.name;
50
+ return name === "NotFound" || name === "NoSuchKey" || name === "404";
51
+ }
52
+ function isAccessDeniedError(error) {
53
+ if (!error || typeof error !== "object") return false;
54
+ const err = error;
55
+ return err.name === "AccessDenied" || err.$metadata?.httpStatusCode === 403;
56
+ }
57
+ function trimSlashes(s) {
58
+ let start = 0;
59
+ let end = s.length;
60
+ while (start < end && s[start] === "/") start++;
61
+ while (end > start && s[end - 1] === "/") end--;
62
+ return s.slice(start, end);
63
+ }
64
+ var S3Filesystem = class extends MastraFilesystem {
65
+ id;
66
+ name = "S3Filesystem";
67
+ provider = "s3";
68
+ readOnly;
69
+ status = "pending";
70
+ // Display metadata for UI
71
+ displayName;
72
+ icon = "s3";
73
+ description;
74
+ bucket;
75
+ region;
76
+ accessKeyId;
77
+ secretAccessKey;
78
+ endpoint;
79
+ forcePathStyle;
80
+ prefix;
81
+ _client = null;
82
+ constructor(options) {
83
+ super({ name: "S3Filesystem" });
84
+ this.id = options.id ?? `s3-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
85
+ this.bucket = options.bucket;
86
+ this.region = options.region;
87
+ this.accessKeyId = options.accessKeyId;
88
+ this.secretAccessKey = options.secretAccessKey;
89
+ this.endpoint = options.endpoint;
90
+ this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
91
+ this.prefix = options.prefix ? trimSlashes(options.prefix) + "/" : "";
92
+ this.icon = options.icon ?? this.detectIconFromEndpoint(options.endpoint);
93
+ this.displayName = options.displayName ?? this.getDefaultDisplayName(this.icon);
94
+ this.description = options.description;
95
+ this.readOnly = options.readOnly;
96
+ }
97
+ /**
98
+ * Get mount configuration for E2B sandbox.
99
+ * Returns S3-compatible config that works with s3fs-fuse.
100
+ */
101
+ getMountConfig() {
102
+ const config = {
103
+ type: "s3",
104
+ bucket: this.bucket,
105
+ region: this.region,
106
+ endpoint: this.endpoint
107
+ };
108
+ if (this.accessKeyId && this.secretAccessKey) {
109
+ config.accessKeyId = this.accessKeyId;
110
+ config.secretAccessKey = this.secretAccessKey;
111
+ }
112
+ if (this.readOnly) {
113
+ config.readOnly = true;
114
+ }
115
+ return config;
116
+ }
117
+ /**
118
+ * Get filesystem info for status reporting.
119
+ */
120
+ getInfo() {
121
+ return {
122
+ id: this.id,
123
+ name: this.name,
124
+ provider: this.provider,
125
+ status: this.status,
126
+ error: this.error,
127
+ icon: this.icon,
128
+ metadata: {
129
+ bucket: this.bucket,
130
+ region: this.region,
131
+ ...this.endpoint && { endpoint: this.endpoint },
132
+ ...this.prefix && { prefix: this.prefix }
133
+ }
134
+ };
135
+ }
136
+ /**
137
+ * Handle an error, checking for access denied and updating status accordingly.
138
+ * Returns the error for re-throwing.
139
+ */
140
+ handleError(error) {
141
+ if (isAccessDeniedError(error)) {
142
+ this.status = "error";
143
+ this.error = "Access denied - check credentials and bucket permissions";
144
+ }
145
+ return error;
146
+ }
147
+ /**
148
+ * Get instructions describing this S3 filesystem.
149
+ * Used by agents to understand storage semantics.
150
+ */
151
+ getInstructions() {
152
+ const providerName = this.displayName || "S3";
153
+ const access = this.readOnly ? "Read-only" : "Persistent";
154
+ return `${providerName} storage in bucket "${this.bucket}". ${access} storage - files are retained across sessions.`;
155
+ }
156
+ /**
157
+ * Detect the appropriate icon based on the S3 endpoint.
158
+ */
159
+ detectIconFromEndpoint(endpoint) {
160
+ if (!endpoint) {
161
+ return "aws-s3";
162
+ }
163
+ let hostname;
164
+ try {
165
+ const url = new URL(endpoint);
166
+ hostname = url.hostname.toLowerCase();
167
+ } catch {
168
+ hostname = endpoint.toLowerCase();
169
+ }
170
+ if (hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com") || hostname.endsWith(".cloudflare.com")) {
171
+ return "r2";
172
+ }
173
+ if (hostname === "storage.googleapis.com" || hostname.endsWith(".storage.googleapis.com") || hostname.endsWith(".googleapis.com")) {
174
+ return "gcs";
175
+ }
176
+ if (hostname === "blob.core.windows.net" || hostname.endsWith(".blob.core.windows.net") || hostname.endsWith(".azure.com")) {
177
+ return "azure";
178
+ }
179
+ if (hostname.includes("minio")) {
180
+ return "minio";
181
+ }
182
+ return "s3";
183
+ }
184
+ /**
185
+ * Get a user-friendly display name based on the icon/provider.
186
+ */
187
+ getDefaultDisplayName(icon) {
188
+ switch (icon) {
189
+ case "aws-s3":
190
+ return "AWS S3";
191
+ case "r2":
192
+ case "cloudflare":
193
+ case "cloudflare-r2":
194
+ return "Cloudflare R2";
195
+ case "gcs":
196
+ case "google-cloud":
197
+ case "google-cloud-storage":
198
+ return "Google Cloud Storage";
199
+ case "azure":
200
+ case "azure-blob":
201
+ return "Azure Blob";
202
+ case "minio":
203
+ return "MinIO";
204
+ case "s3":
205
+ return "S3";
206
+ default:
207
+ return void 0;
208
+ }
209
+ }
210
+ getClient() {
211
+ if (this._client) return this._client;
212
+ const hasCredentials = this.accessKeyId && this.secretAccessKey;
213
+ this._client = new S3Client({
214
+ region: this.region,
215
+ credentials: hasCredentials ? {
216
+ accessKeyId: this.accessKeyId,
217
+ secretAccessKey: this.secretAccessKey
218
+ } : (
219
+ // Anonymous access for public buckets - use empty credentials
220
+ // to prevent SDK from trying to find credentials elsewhere
221
+ { accessKeyId: "", secretAccessKey: "" }
222
+ ),
223
+ endpoint: this.endpoint,
224
+ forcePathStyle: this.forcePathStyle,
225
+ // Skip signing for anonymous access (public buckets).
226
+ // No-op signer passes the request through unsigned. Uses `any` because
227
+ // the correct type (HttpRequest from @smithy/types) is not a direct dependency.
228
+ ...hasCredentials ? {} : { signer: { sign: async (request) => request } }
229
+ });
230
+ return this._client;
231
+ }
232
+ /**
233
+ * Ensure the filesystem is initialized and return the S3 client.
234
+ * Uses base class ensureReady() for status management, then returns client.
235
+ */
236
+ async getReadyClient() {
237
+ await this.ensureReady();
238
+ return this.getClient();
239
+ }
240
+ toKey(path) {
241
+ const cleanPath = path.replace(/^\/+/, "");
242
+ return this.prefix + cleanPath;
243
+ }
244
+ // ---------------------------------------------------------------------------
245
+ // File Operations
246
+ // ---------------------------------------------------------------------------
247
+ async readFile(path, options) {
248
+ const client = await this.getReadyClient();
249
+ try {
250
+ const response = await client.send(
251
+ new GetObjectCommand({
252
+ Bucket: this.bucket,
253
+ Key: this.toKey(path)
254
+ })
255
+ );
256
+ const body = await response.Body?.transformToByteArray();
257
+ if (!body) throw new FileNotFoundError(path);
258
+ const buffer = Buffer.from(body);
259
+ if (options?.encoding) {
260
+ return buffer.toString(options.encoding);
261
+ }
262
+ return buffer;
263
+ } catch (error) {
264
+ if (isNotFoundError(error)) {
265
+ throw new FileNotFoundError(path);
266
+ }
267
+ throw this.handleError(error);
268
+ }
269
+ }
270
+ async writeFile(path, content, _options) {
271
+ const client = await this.getReadyClient();
272
+ const body = typeof content === "string" ? Buffer.from(content, "utf-8") : Buffer.from(content);
273
+ const contentType = getMimeType(path);
274
+ await client.send(
275
+ new PutObjectCommand({
276
+ Bucket: this.bucket,
277
+ Key: this.toKey(path),
278
+ Body: body,
279
+ ContentType: contentType
280
+ })
281
+ );
282
+ }
283
+ async appendFile(path, content) {
284
+ let existing = "";
285
+ try {
286
+ existing = await this.readFile(path, { encoding: "utf-8" });
287
+ } catch (error) {
288
+ if (error instanceof FileNotFoundError) ; else {
289
+ throw error;
290
+ }
291
+ }
292
+ const appendContent = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
293
+ await this.writeFile(path, existing + appendContent);
294
+ }
295
+ async deleteFile(path, options) {
296
+ const isDir = await this.isDirectory(path);
297
+ if (isDir) {
298
+ await this.rmdir(path, { recursive: true, force: options?.force });
299
+ return;
300
+ }
301
+ const client = await this.getReadyClient();
302
+ try {
303
+ await client.send(
304
+ new DeleteObjectCommand({
305
+ Bucket: this.bucket,
306
+ Key: this.toKey(path)
307
+ })
308
+ );
309
+ } catch (error) {
310
+ if (options?.force) return;
311
+ if (isNotFoundError(error)) {
312
+ throw new FileNotFoundError(path);
313
+ }
314
+ throw this.handleError(error);
315
+ }
316
+ }
317
+ async copyFile(src, dest, _options) {
318
+ const client = await this.getReadyClient();
319
+ try {
320
+ await client.send(
321
+ new CopyObjectCommand({
322
+ Bucket: this.bucket,
323
+ CopySource: `${this.bucket}/${encodeURIComponent(this.toKey(src)).replace(/%2F/g, "/")}`,
324
+ Key: this.toKey(dest)
325
+ })
326
+ );
327
+ } catch (error) {
328
+ if (isNotFoundError(error)) {
329
+ throw new FileNotFoundError(src);
330
+ }
331
+ throw this.handleError(error);
332
+ }
333
+ }
334
+ async moveFile(src, dest, options) {
335
+ await this.copyFile(src, dest, options);
336
+ await this.deleteFile(src, { force: true });
337
+ }
338
+ // ---------------------------------------------------------------------------
339
+ // Directory Operations
340
+ // ---------------------------------------------------------------------------
341
+ async mkdir(_path, _options) {
342
+ }
343
+ async rmdir(path, options) {
344
+ if (!options?.recursive) {
345
+ const entries = await this.readdir(path);
346
+ if (entries.length > 0) {
347
+ throw new Error(`Directory not empty: ${path}`);
348
+ }
349
+ return;
350
+ }
351
+ const client = await this.getReadyClient();
352
+ const prefix = this.toKey(path).replace(/\/$/, "") + "/";
353
+ let continuationToken;
354
+ do {
355
+ const listResponse = await client.send(
356
+ new ListObjectsV2Command({
357
+ Bucket: this.bucket,
358
+ Prefix: prefix,
359
+ ContinuationToken: continuationToken
360
+ })
361
+ );
362
+ if (listResponse.Contents && listResponse.Contents.length > 0) {
363
+ const deleteResponse = await client.send(
364
+ new DeleteObjectsCommand({
365
+ Bucket: this.bucket,
366
+ Delete: {
367
+ Objects: listResponse.Contents.filter((obj) => !!obj.Key).map((obj) => ({
368
+ Key: obj.Key
369
+ }))
370
+ }
371
+ })
372
+ );
373
+ if (deleteResponse.Errors && deleteResponse.Errors.length > 0) {
374
+ throw new Error(`Failed to delete ${deleteResponse.Errors.length} object(s) in ${path}`);
375
+ }
376
+ }
377
+ continuationToken = listResponse.NextContinuationToken;
378
+ } while (continuationToken);
379
+ }
380
+ async readdir(path, options) {
381
+ const client = await this.getReadyClient();
382
+ const prefix = this.toKey(path).replace(/\/$/, "");
383
+ const searchPrefix = prefix ? prefix + "/" : "";
384
+ const entries = [];
385
+ const seenDirs = /* @__PURE__ */ new Set();
386
+ let continuationToken;
387
+ do {
388
+ const response = await client.send(
389
+ new ListObjectsV2Command({
390
+ Bucket: this.bucket,
391
+ Prefix: searchPrefix,
392
+ Delimiter: options?.recursive ? void 0 : "/",
393
+ ContinuationToken: continuationToken
394
+ })
395
+ );
396
+ if (response.Contents) {
397
+ for (const obj of response.Contents) {
398
+ const key = obj.Key;
399
+ if (!key || key === searchPrefix) continue;
400
+ const relativePath = key.slice(searchPrefix.length);
401
+ if (!relativePath) continue;
402
+ if (relativePath.endsWith("/")) {
403
+ const dirName = relativePath.slice(0, -1);
404
+ if (!seenDirs.has(dirName)) {
405
+ seenDirs.add(dirName);
406
+ entries.push({ name: dirName, type: "directory" });
407
+ }
408
+ continue;
409
+ }
410
+ const name = options?.recursive ? relativePath : relativePath.split("/")[0];
411
+ if (!name) continue;
412
+ if (options?.extension) {
413
+ const extensions = Array.isArray(options.extension) ? options.extension : [options.extension];
414
+ if (!extensions.some((ext) => name.endsWith(ext))) {
415
+ continue;
416
+ }
417
+ }
418
+ entries.push({
419
+ name,
420
+ type: "file",
421
+ size: obj.Size
422
+ });
423
+ }
424
+ }
425
+ if (response.CommonPrefixes) {
426
+ for (const prefixObj of response.CommonPrefixes) {
427
+ if (!prefixObj.Prefix) continue;
428
+ const dirName = prefixObj.Prefix.slice(searchPrefix.length).replace(/\/$/, "");
429
+ if (dirName && !seenDirs.has(dirName)) {
430
+ seenDirs.add(dirName);
431
+ entries.push({ name: dirName, type: "directory" });
432
+ }
433
+ }
434
+ }
435
+ continuationToken = response.NextContinuationToken;
436
+ } while (continuationToken);
437
+ return entries;
438
+ }
439
+ // ---------------------------------------------------------------------------
440
+ // Path Operations
441
+ // ---------------------------------------------------------------------------
442
+ async exists(path) {
443
+ const key = this.toKey(path);
444
+ if (!key) return true;
445
+ const client = await this.getReadyClient();
446
+ try {
447
+ await client.send(
448
+ new HeadObjectCommand({
449
+ Bucket: this.bucket,
450
+ Key: key
451
+ })
452
+ );
453
+ return true;
454
+ } catch (error) {
455
+ if (!isNotFoundError(error)) throw this.handleError(error);
456
+ }
457
+ const response = await client.send(
458
+ new ListObjectsV2Command({
459
+ Bucket: this.bucket,
460
+ Prefix: key.replace(/\/$/, "") + "/",
461
+ MaxKeys: 1
462
+ })
463
+ );
464
+ return (response.Contents?.length ?? 0) > 0;
465
+ }
466
+ async stat(path) {
467
+ const key = this.toKey(path);
468
+ if (!key) {
469
+ return {
470
+ name: "",
471
+ path,
472
+ type: "directory",
473
+ size: 0,
474
+ createdAt: /* @__PURE__ */ new Date(),
475
+ modifiedAt: /* @__PURE__ */ new Date()
476
+ };
477
+ }
478
+ const client = await this.getReadyClient();
479
+ try {
480
+ const response = await client.send(
481
+ new HeadObjectCommand({
482
+ Bucket: this.bucket,
483
+ Key: key
484
+ })
485
+ );
486
+ const name = path.split("/").pop() ?? "";
487
+ return {
488
+ name,
489
+ path,
490
+ type: "file",
491
+ size: response.ContentLength ?? 0,
492
+ createdAt: response.LastModified ?? /* @__PURE__ */ new Date(),
493
+ modifiedAt: response.LastModified ?? /* @__PURE__ */ new Date()
494
+ };
495
+ } catch (error) {
496
+ if (!isNotFoundError(error)) throw this.handleError(error);
497
+ const isDir = await this.isDirectory(path);
498
+ if (isDir) {
499
+ const name = path.split("/").filter(Boolean).pop() ?? "";
500
+ return {
501
+ name,
502
+ path,
503
+ type: "directory",
504
+ size: 0,
505
+ createdAt: /* @__PURE__ */ new Date(),
506
+ modifiedAt: /* @__PURE__ */ new Date()
507
+ };
508
+ }
509
+ throw new FileNotFoundError(path);
510
+ }
511
+ }
512
+ async isFile(path) {
513
+ const key = this.toKey(path);
514
+ if (!key) return false;
515
+ const client = await this.getReadyClient();
516
+ try {
517
+ await client.send(
518
+ new HeadObjectCommand({
519
+ Bucket: this.bucket,
520
+ Key: key
521
+ })
522
+ );
523
+ return true;
524
+ } catch (error) {
525
+ if (!isNotFoundError(error)) throw this.handleError(error);
526
+ return false;
527
+ }
528
+ }
529
+ async isDirectory(path) {
530
+ const key = this.toKey(path);
531
+ if (!key) return true;
532
+ const client = await this.getReadyClient();
533
+ const response = await client.send(
534
+ new ListObjectsV2Command({
535
+ Bucket: this.bucket,
536
+ Prefix: key.replace(/\/$/, "") + "/",
537
+ MaxKeys: 1
538
+ })
539
+ );
540
+ return (response.Contents?.length ?? 0) > 0;
541
+ }
542
+ // ---------------------------------------------------------------------------
543
+ // Lifecycle (overrides base class protected methods)
544
+ // ---------------------------------------------------------------------------
545
+ /**
546
+ * Initialize the S3 client.
547
+ * Status management is handled by the base class.
548
+ */
549
+ async init() {
550
+ const client = this.getClient();
551
+ try {
552
+ await client.send(new HeadBucketCommand({ Bucket: this.bucket }));
553
+ } catch (error) {
554
+ const statusCode = error.$metadata?.httpStatusCode;
555
+ const createError = (message2) => {
556
+ const err = new Error(message2);
557
+ if (statusCode) err.status = statusCode;
558
+ return err;
559
+ };
560
+ if (isAccessDeniedError(error)) {
561
+ throw createError(`Access denied to bucket "${this.bucket}" - check credentials and permissions`);
562
+ }
563
+ if (isNotFoundError(error)) {
564
+ throw createError(`Bucket "${this.bucket}" not found`);
565
+ }
566
+ const message = error instanceof Error ? error.message : String(error);
567
+ if (statusCode) {
568
+ throw createError(`Failed to access bucket "${this.bucket}" (HTTP ${statusCode}): ${message}`);
569
+ }
570
+ throw error;
571
+ }
572
+ }
573
+ /**
574
+ * Clean up the S3 client.
575
+ * Status management is handled by the base class.
576
+ */
577
+ async destroy() {
578
+ this._client = null;
579
+ }
580
+ };
581
+
582
+ export { S3Filesystem };
583
+ //# sourceMappingURL=index.js.map
584
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/filesystem/index.ts"],"names":["message"],"mappings":";;;;AAmDA,IAAM,UAAA,GAAqC;AAAA;AAAA,EAEzC,MAAA,EAAQ,YAAA;AAAA,EACR,KAAA,EAAO,eAAA;AAAA,EACP,WAAA,EAAa,eAAA;AAAA,EACb,OAAA,EAAS,WAAA;AAAA,EACT,MAAA,EAAQ,WAAA;AAAA,EACR,MAAA,EAAQ,UAAA;AAAA,EACR,MAAA,EAAQ,UAAA;AAAA,EACR,MAAA,EAAQ,UAAA;AAAA;AAAA,EAER,KAAA,EAAO,iBAAA;AAAA,EACP,MAAA,EAAQ,iBAAA;AAAA,EACR,KAAA,EAAO,iBAAA;AAAA,EACP,MAAA,EAAQ,iBAAA;AAAA,EACR,MAAA,EAAQ,iBAAA;AAAA,EACR,OAAA,EAAS,kBAAA;AAAA,EACT,OAAA,EAAS,WAAA;AAAA,EACT,MAAA,EAAQ,WAAA;AAAA,EACR,KAAA,EAAO,eAAA;AAAA,EACP,KAAA,EAAO,aAAA;AAAA,EACP,KAAA,EAAO,oBAAA;AAAA,EACP,OAAA,EAAS,oBAAA;AAAA;AAAA,EAET,MAAA,EAAQ,WAAA;AAAA,EACR,MAAA,EAAQ,YAAA;AAAA,EACR,OAAA,EAAS,YAAA;AAAA,EACT,MAAA,EAAQ,WAAA;AAAA,EACR,MAAA,EAAQ,eAAA;AAAA,EACR,OAAA,EAAS,YAAA;AAAA,EACT,MAAA,EAAQ,cAAA;AAAA;AAAA,EAER,MAAA,EAAQ,iBAAA;AAAA;AAAA,EAER,MAAA,EAAQ,iBAAA;AAAA,EACR,KAAA,EAAO,kBAAA;AAAA,EACP,MAAA,EAAQ;AACV,CAAA;AAKA,SAAS,YAAY,IAAA,EAAsB;AACzC,EAAA,MAAM,MAAM,IAAA,CAAK,WAAA,GAAc,KAAA,CAAM,UAAU,IAAI,CAAC,CAAA;AACpD,EAAA,OAAO,GAAA,GAAO,UAAA,CAAW,GAAG,CAAA,IAAK,0BAAA,GAA8B,0BAAA;AACjE;AAGA,SAAS,gBAAgB,KAAA,EAAyB;AAChD,EAAA,IAAI,CAAC,SAAS,OAAO,KAAA,KAAU,YAAY,EAAE,MAAA,IAAU,QAAQ,OAAO,KAAA;AACtE,EAAA,MAAM,OAAQ,KAAA,CAA2B,IAAA;AACzC,EAAA,OAAO,IAAA,KAAS,UAAA,IAAc,IAAA,KAAS,WAAA,IAAe,IAAA,KAAS,KAAA;AACjE;AAGA,SAAS,oBAAoB,KAAA,EAAyB;AACpD,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,UAAU,OAAO,KAAA;AAChD,EAAA,MAAM,GAAA,GAAM,KAAA;AACZ,EAAA,OAAO,GAAA,CAAI,IAAA,KAAS,cAAA,IAAkB,GAAA,CAAI,WAAW,cAAA,KAAmB,GAAA;AAC1E;AA2FA,SAAS,YAAY,CAAA,EAAmB;AACtC,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,IAAI,MAAM,CAAA,CAAE,MAAA;AACZ,EAAA,OAAO,KAAA,GAAQ,GAAA,IAAO,CAAA,CAAE,KAAK,MAAM,GAAA,EAAK,KAAA,EAAA;AACxC,EAAA,OAAO,MAAM,KAAA,IAAS,CAAA,CAAE,GAAA,GAAM,CAAC,MAAM,GAAA,EAAK,GAAA,EAAA;AAC1C,EAAA,OAAO,CAAA,CAAE,KAAA,CAAM,KAAA,EAAO,GAAG,CAAA;AAC3B;AAEO,IAAM,YAAA,GAAN,cAA2B,gBAAA,CAAiB;AAAA,EACxC,EAAA;AAAA,EACA,IAAA,GAAO,cAAA;AAAA,EACP,QAAA,GAAW,IAAA;AAAA,EACX,QAAA;AAAA,EAET,MAAA,GAAyB,SAAA;AAAA;AAAA,EAGhB,WAAA;AAAA,EACA,IAAA,GAAuB,IAAA;AAAA,EACvB,WAAA;AAAA,EAEQ,MAAA;AAAA,EACA,MAAA;AAAA,EACA,WAAA;AAAA,EACA,eAAA;AAAA,EACA,QAAA;AAAA,EACA,cAAA;AAAA,EACA,MAAA;AAAA,EAET,OAAA,GAA2B,IAAA;AAAA,EAEnC,YAAY,OAAA,EAA8B;AACxC,IAAA,KAAA,CAAM,EAAE,IAAA,EAAM,cAAA,EAAgB,CAAA;AAC9B,IAAA,IAAA,CAAK,EAAA,GAAK,QAAQ,EAAA,IAAM,CAAA,MAAA,EAAS,KAAK,GAAA,EAAI,CAAE,SAAS,EAAE,CAAC,IAAI,IAAA,CAAK,MAAA,GAAS,QAAA,CAAS,EAAE,EAAE,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AAClG,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,cAAc,OAAA,CAAQ,WAAA;AAC3B,IAAA,IAAA,CAAK,kBAAkB,OAAA,CAAQ,eAAA;AAC/B,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AACxB,IAAA,IAAA,CAAK,cAAA,GAAiB,OAAA,CAAQ,cAAA,IAAkB,CAAC,CAAC,OAAA,CAAQ,QAAA;AAE1D,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA,GAAS,YAAY,OAAA,CAAQ,MAAM,IAAI,GAAA,GAAM,EAAA;AAGnE,IAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,IAAA,IAAQ,IAAA,CAAK,sBAAA,CAAuB,QAAQ,QAAQ,CAAA;AACxE,IAAA,IAAA,CAAK,cAAc,OAAA,CAAQ,WAAA,IAAe,IAAA,CAAK,qBAAA,CAAsB,KAAK,IAAI,CAAA;AAC9E,IAAA,IAAA,CAAK,cAAc,OAAA,CAAQ,WAAA;AAC3B,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAA,GAAgC;AAC9B,IAAA,MAAM,MAAA,GAAwB;AAAA,MAC5B,IAAA,EAAM,IAAA;AAAA,MACN,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,UAAU,IAAA,CAAK;AAAA,KACjB;AAEA,IAAA,IAAI,IAAA,CAAK,WAAA,IAAe,IAAA,CAAK,eAAA,EAAiB;AAC5C,MAAA,MAAA,CAAO,cAAc,IAAA,CAAK,WAAA;AAC1B,MAAA,MAAA,CAAO,kBAAkB,IAAA,CAAK,eAAA;AAAA,IAChC;AAEA,IAAA,IAAI,KAAK,QAAA,EAAU;AACjB,MAAA,MAAA,CAAO,QAAA,GAAW,IAAA;AAAA,IACpB;AAEA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,OAAA,GAA0B;AACxB,IAAA,OAAO;AAAA,MACL,IAAI,IAAA,CAAK,EAAA;AAAA,MACT,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,QAAA,EAAU;AAAA,QACR,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,GAAI,IAAA,CAAK,QAAA,IAAY,EAAE,QAAA,EAAU,KAAK,QAAA,EAAS;AAAA,QAC/C,GAAI,IAAA,CAAK,MAAA,IAAU,EAAE,MAAA,EAAQ,KAAK,MAAA;AAAO;AAC3C,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,YAAY,KAAA,EAAyB;AAC3C,IAAA,IAAI,mBAAA,CAAoB,KAAK,CAAA,EAAG;AAC9B,MAAA,IAAA,CAAK,MAAA,GAAS,OAAA;AACd,MAAA,IAAA,CAAK,KAAA,GAAQ,0DAAA;AAAA,IACf;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAA,GAA0B;AACxB,IAAA,MAAM,YAAA,GAAe,KAAK,WAAA,IAAe,IAAA;AACzC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,GAAW,WAAA,GAAc,YAAA;AAC7C,IAAA,OAAO,GAAG,YAAY,CAAA,oBAAA,EAAuB,IAAA,CAAK,MAAM,MAAM,MAAM,CAAA,8CAAA,CAAA;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA,EAKQ,uBAAuB,QAAA,EAAmC;AAChE,IAAA,IAAI,CAAC,QAAA,EAAU;AAEb,MAAA,OAAO,QAAA;AAAA,IACT;AAGA,IAAA,IAAI,QAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,QAAQ,CAAA;AAC5B,MAAA,QAAA,GAAW,GAAA,CAAI,SAAS,WAAA,EAAY;AAAA,IACtC,CAAA,CAAA,MAAQ;AAEN,MAAA,QAAA,GAAW,SAAS,WAAA,EAAY;AAAA,IAClC;AAGA,IAAA,IACE,QAAA,KAAa,8BACb,QAAA,CAAS,QAAA,CAAS,2BAA2B,CAAA,IAC7C,QAAA,CAAS,QAAA,CAAS,iBAAiB,CAAA,EACnC;AACA,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,IACE,QAAA,KAAa,4BACb,QAAA,CAAS,QAAA,CAAS,yBAAyB,CAAA,IAC3C,QAAA,CAAS,QAAA,CAAS,iBAAiB,CAAA,EACnC;AACA,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IACE,QAAA,KAAa,2BACb,QAAA,CAAS,QAAA,CAAS,wBAAwB,CAAA,IAC1C,QAAA,CAAS,QAAA,CAAS,YAAY,CAAA,EAC9B;AACA,MAAA,OAAO,OAAA;AAAA,IACT;AAEA,IAAA,IAAI,QAAA,CAAS,QAAA,CAAS,OAAO,CAAA,EAAG;AAC9B,MAAA,OAAO,OAAA;AAAA,IACT;AAGA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAAsB,IAAA,EAA0C;AACtE,IAAA,QAAQ,IAAA;AAAM,MACZ,KAAK,QAAA;AACH,QAAA,OAAO,QAAA;AAAA,MACT,KAAK,IAAA;AAAA,MACL,KAAK,YAAA;AAAA,MACL,KAAK,eAAA;AACH,QAAA,OAAO,eAAA;AAAA,MACT,KAAK,KAAA;AAAA,MACL,KAAK,cAAA;AAAA,MACL,KAAK,sBAAA;AACH,QAAA,OAAO,sBAAA;AAAA,MACT,KAAK,OAAA;AAAA,MACL,KAAK,YAAA;AACH,QAAA,OAAO,YAAA;AAAA,MACT,KAAK,OAAA;AACH,QAAA,OAAO,OAAA;AAAA,MACT,KAAK,IAAA;AACH,QAAA,OAAO,IAAA;AAAA,MACT;AAEE,QAAA,OAAO,MAAA;AAAA;AACX,EACF;AAAA,EAEQ,SAAA,GAAsB;AAC5B,IAAA,IAAI,IAAA,CAAK,OAAA,EAAS,OAAO,IAAA,CAAK,OAAA;AAE9B,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,WAAA,IAAe,IAAA,CAAK,eAAA;AAEhD,IAAA,IAAA,CAAK,OAAA,GAAU,IAAI,QAAA,CAAS;AAAA,MAC1B,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,aAAa,cAAA,GACT;AAAA,QACE,aAAa,IAAA,CAAK,WAAA;AAAA,QAClB,iBAAiB,IAAA,CAAK;AAAA,OACxB;AAAA;AAAA;AAAA,QAGA,EAAE,WAAA,EAAa,EAAA,EAAI,eAAA,EAAiB,EAAA;AAAG,OAAA;AAAA,MAC3C,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,gBAAgB,IAAA,CAAK,cAAA;AAAA;AAAA;AAAA;AAAA,MAKrB,GAAI,cAAA,GAAiB,EAAC,GAAI,EAAE,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAO,OAAA,KAAiB,OAAA,EAAQ;AAAE,KAC/E,CAAA;AAED,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,cAAA,GAAoC;AAChD,IAAA,MAAM,KAAK,WAAA,EAAY;AACvB,IAAA,OAAO,KAAK,SAAA,EAAU;AAAA,EACxB;AAAA,EAEQ,MAAM,IAAA,EAAsB;AAElC,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACzC,IAAA,OAAO,KAAK,MAAA,GAAS,SAAA;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAA,CAAS,IAAA,EAAc,OAAA,EAAiD;AAC5E,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,IAAA;AAAA,QAC5B,IAAI,gBAAA,CAAiB;AAAA,UACnB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,IAAI;AAAA,SACrB;AAAA,OACH;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAM,oBAAA,EAAqB;AACvD,MAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,kBAAkB,IAAI,CAAA;AAE3C,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA;AAC/B,MAAA,IAAI,SAAS,QAAA,EAAU;AACrB,QAAA,OAAO,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,QAAQ,CAAA;AAAA,MACzC;AACA,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,KAAA,EAAgB;AACvB,MAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG;AAC1B,QAAA,MAAM,IAAI,kBAAkB,IAAI,CAAA;AAAA,MAClC;AACA,MAAA,MAAM,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAM,SAAA,CAAU,IAAA,EAAc,OAAA,EAAsB,QAAA,EAAwC;AAC1F,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,MAAM,IAAA,GAAO,OAAO,OAAA,KAAY,QAAA,GAAW,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,OAAO,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA;AAC9F,IAAA,MAAM,WAAA,GAAc,YAAY,IAAI,CAAA;AAEpC,IAAA,MAAM,MAAA,CAAO,IAAA;AAAA,MACX,IAAI,gBAAA,CAAiB;AAAA,QACnB,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAAA,QACpB,IAAA,EAAM,IAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACd;AAAA,KACH;AAAA,EACF;AAAA,EAEA,MAAM,UAAA,CAAW,IAAA,EAAc,OAAA,EAAqC;AAElE,IAAA,IAAI,QAAA,GAAW,EAAA;AACf,IAAA,IAAI;AACF,MAAA,QAAA,GAAY,MAAM,IAAA,CAAK,QAAA,CAAS,MAAM,EAAE,QAAA,EAAU,SAAS,CAAA;AAAA,IAC7D,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,iBAAiB,iBAAA,EAAmB,CAExC,MAAO;AACL,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,IACF;AAEA,IAAA,MAAM,aAAA,GAAgB,OAAO,OAAA,KAAY,QAAA,GAAW,OAAA,GAAU,OAAO,IAAA,CAAK,OAAO,CAAA,CAAE,QAAA,CAAS,OAAO,CAAA;AACnG,IAAA,MAAM,IAAA,CAAK,SAAA,CAAU,IAAA,EAAM,QAAA,GAAW,aAAa,CAAA;AAAA,EACrD;AAAA,EAEA,MAAM,UAAA,CAAW,IAAA,EAAc,OAAA,EAAwC;AAErE,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,WAAA,CAAY,IAAI,CAAA;AACzC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAM,IAAA,CAAK,MAAM,IAAA,EAAM,EAAE,WAAW,IAAA,EAAM,KAAA,EAAO,OAAA,EAAS,KAAA,EAAO,CAAA;AACjE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,CAAO,IAAA;AAAA,QACX,IAAI,mBAAA,CAAoB;AAAA,UACtB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,IAAI;AAAA,SACrB;AAAA,OACH;AAAA,IACF,SAAS,KAAA,EAAgB;AACvB,MAAA,IAAI,SAAS,KAAA,EAAO;AACpB,MAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG;AAC1B,QAAA,MAAM,IAAI,kBAAkB,IAAI,CAAA;AAAA,MAClC;AACA,MAAA,MAAM,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,CAAS,GAAA,EAAa,IAAA,EAAc,QAAA,EAAuC;AAC/E,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,CAAO,IAAA;AAAA,QACX,IAAI,iBAAA,CAAkB;AAAA,UACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,UAAA,EAAY,CAAA,EAAG,IAAA,CAAK,MAAM,IAAI,kBAAA,CAAmB,IAAA,CAAK,KAAA,CAAM,GAAG,CAAC,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,GAAG,CAAC,CAAA,CAAA;AAAA,UACtF,GAAA,EAAK,IAAA,CAAK,KAAA,CAAM,IAAI;AAAA,SACrB;AAAA,OACH;AAAA,IACF,SAAS,KAAA,EAAgB;AACvB,MAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG;AAC1B,QAAA,MAAM,IAAI,kBAAkB,GAAG,CAAA;AAAA,MACjC;AACA,MAAA,MAAM,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,CAAS,GAAA,EAAa,IAAA,EAAc,OAAA,EAAsC;AAC9E,IAAA,MAAM,IAAA,CAAK,QAAA,CAAS,GAAA,EAAK,IAAA,EAAM,OAAO,CAAA;AACtC,IAAA,MAAM,KAAK,UAAA,CAAW,GAAA,EAAK,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,KAAA,CAAM,KAAA,EAAe,QAAA,EAAmD;AAAA,EAG9E;AAAA,EAEA,MAAM,KAAA,CAAM,IAAA,EAAc,OAAA,EAAwC;AAChE,IAAA,IAAI,CAAC,SAAS,SAAA,EAAW;AAEvB,MAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAI,CAAA;AACvC,MAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,qBAAA,EAAwB,IAAI,CAAA,CAAE,CAAA;AAAA,MAChD;AACA,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAI,EAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,GAAI,GAAA;AAErD,IAAA,IAAI,iBAAA;AACJ,IAAA,GAAG;AACD,MAAA,MAAM,YAAA,GAAe,MAAM,MAAA,CAAO,IAAA;AAAA,QAChC,IAAI,oBAAA,CAAqB;AAAA,UACvB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,MAAA,EAAQ,MAAA;AAAA,UACR,iBAAA,EAAmB;AAAA,SACpB;AAAA,OACH;AAEA,MAAA,IAAI,YAAA,CAAa,QAAA,IAAY,YAAA,CAAa,QAAA,CAAS,SAAS,CAAA,EAAG;AAC7D,QAAA,MAAM,cAAA,GAAiB,MAAM,MAAA,CAAO,IAAA;AAAA,UAClC,IAAI,oBAAA,CAAqB;AAAA,YACvB,QAAQ,IAAA,CAAK,MAAA;AAAA,YACb,MAAA,EAAQ;AAAA,cACN,OAAA,EAAS,YAAA,CAAa,QAAA,CAAS,MAAA,CAAO,CAAC,GAAA,KAAgC,CAAC,CAAC,GAAA,CAAI,GAAG,CAAA,CAAE,GAAA,CAAI,CAAA,GAAA,MAAQ;AAAA,gBAC5F,KAAK,GAAA,CAAI;AAAA,eACX,CAAE;AAAA;AACJ,WACD;AAAA,SACH;AACA,QAAA,IAAI,cAAA,CAAe,MAAA,IAAU,cAAA,CAAe,MAAA,CAAO,SAAS,CAAA,EAAG;AAC7D,UAAA,MAAM,IAAI,MAAM,CAAA,iBAAA,EAAoB,cAAA,CAAe,OAAO,MAAM,CAAA,cAAA,EAAiB,IAAI,CAAA,CAAE,CAAA;AAAA,QACzF;AAAA,MACF;AAEA,MAAA,iBAAA,GAAoB,YAAA,CAAa,qBAAA;AAAA,IACnC,CAAA,QAAS,iBAAA;AAAA,EACX;AAAA,EAEA,MAAM,OAAA,CAAQ,IAAA,EAAc,OAAA,EAA6C;AACvE,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,MAAM,SAAS,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AACjD,IAAA,MAAM,YAAA,GAAe,MAAA,GAAS,MAAA,GAAS,GAAA,GAAM,EAAA;AAE7C,IAAA,MAAM,UAAuB,EAAC;AAC9B,IAAA,MAAM,QAAA,uBAAe,GAAA,EAAY;AAEjC,IAAA,IAAI,iBAAA;AACJ,IAAA,GAAG;AACD,MAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,IAAA;AAAA,QAC5B,IAAI,oBAAA,CAAqB;AAAA,UACvB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,MAAA,EAAQ,YAAA;AAAA,UACR,SAAA,EAAW,OAAA,EAAS,SAAA,GAAY,MAAA,GAAY,GAAA;AAAA,UAC5C,iBAAA,EAAmB;AAAA,SACpB;AAAA,OACH;AAGA,MAAA,IAAI,SAAS,QAAA,EAAU;AACrB,QAAA,KAAA,MAAW,GAAA,IAAO,SAAS,QAAA,EAAU;AACnC,UAAA,MAAM,MAAM,GAAA,CAAI,GAAA;AAChB,UAAA,IAAI,CAAC,GAAA,IAAO,GAAA,KAAQ,YAAA,EAAc;AAElC,UAAA,MAAM,YAAA,GAAe,GAAA,CAAI,KAAA,CAAM,YAAA,CAAa,MAAM,CAAA;AAClD,UAAA,IAAI,CAAC,YAAA,EAAc;AAGnB,UAAA,IAAI,YAAA,CAAa,QAAA,CAAS,GAAG,CAAA,EAAG;AAC9B,YAAA,MAAM,OAAA,GAAU,YAAA,CAAa,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACxC,YAAA,IAAI,CAAC,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,EAAG;AAC1B,cAAA,QAAA,CAAS,IAAI,OAAO,CAAA;AACpB,cAAA,OAAA,CAAQ,KAAK,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,aAAa,CAAA;AAAA,YACnD;AACA,YAAA;AAAA,UACF;AAEA,UAAA,MAAM,IAAA,GAAO,SAAS,SAAA,GAAY,YAAA,GAAe,aAAa,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AAG1E,UAAA,IAAI,CAAC,IAAA,EAAM;AAGX,UAAA,IAAI,SAAS,SAAA,EAAW;AACtB,YAAA,MAAM,UAAA,GAAa,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,SAAS,IAAI,OAAA,CAAQ,SAAA,GAAY,CAAC,OAAA,CAAQ,SAAS,CAAA;AAC5F,YAAA,IAAI,CAAC,WAAW,IAAA,CAAK,CAAA,GAAA,KAAO,KAAK,QAAA,CAAS,GAAG,CAAC,CAAA,EAAG;AAC/C,cAAA;AAAA,YACF;AAAA,UACF;AAEA,UAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,YACX,IAAA;AAAA,YACA,IAAA,EAAM,MAAA;AAAA,YACN,MAAM,GAAA,CAAI;AAAA,WACX,CAAA;AAAA,QACH;AAAA,MACF;AAGA,MAAA,IAAI,SAAS,cAAA,EAAgB;AAC3B,QAAA,KAAA,MAAW,SAAA,IAAa,SAAS,cAAA,EAAgB;AAC/C,UAAA,IAAI,CAAC,UAAU,MAAA,EAAQ;AACvB,UAAA,MAAM,OAAA,GAAU,UAAU,MAAA,CAAO,KAAA,CAAM,aAAa,MAAM,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAC7E,UAAA,IAAI,OAAA,IAAW,CAAC,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,EAAG;AACrC,YAAA,QAAA,CAAS,IAAI,OAAO,CAAA;AACpB,YAAA,OAAA,CAAQ,KAAK,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,aAAa,CAAA;AAAA,UACnD;AAAA,QACF;AAAA,MACF;AAEA,MAAA,iBAAA,GAAoB,QAAA,CAAS,qBAAA;AAAA,IAC/B,CAAA,QAAS,iBAAA;AAET,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,IAAA,EAAgC;AAC3C,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAGzC,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,CAAO,IAAA;AAAA,QACX,IAAI,iBAAA,CAAkB;AAAA,UACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,GAAA,EAAK;AAAA,SACN;AAAA,OACH;AACA,MAAA,OAAO,IAAA;AAAA,IACT,SAAS,KAAA,EAAgB;AACvB,MAAA,IAAI,CAAC,eAAA,CAAgB,KAAK,GAAG,MAAM,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IAE3D;AAGA,IAAA,MAAM,QAAA,GAAqC,MAAM,MAAA,CAAO,IAAA;AAAA,MACtD,IAAI,oBAAA,CAAqB;AAAA,QACvB,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,MAAA,EAAQ,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,GAAI,GAAA;AAAA,QACjC,OAAA,EAAS;AAAA,OACV;AAAA,KACH;AAEA,IAAA,OAAA,CAAQ,QAAA,CAAS,QAAA,EAAU,MAAA,IAAU,CAAA,IAAK,CAAA;AAAA,EAC5C;AAAA,EAEA,MAAM,KAAK,IAAA,EAAiC;AAC1C,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAG3B,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,EAAA;AAAA,QACN,IAAA;AAAA,QACA,IAAA,EAAM,WAAA;AAAA,QACN,IAAA,EAAM,CAAA;AAAA,QACN,SAAA,sBAAe,IAAA,EAAK;AAAA,QACpB,UAAA,sBAAgB,IAAA;AAAK,OACvB;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,IAAI;AACF,MAAA,MAAM,QAAA,GAA4D,MAAM,MAAA,CAAO,IAAA;AAAA,QAC7E,IAAI,iBAAA,CAAkB;AAAA,UACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,GAAA,EAAK;AAAA,SACN;AAAA,OACH;AAEA,MAAA,MAAM,OAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,KAAI,IAAK,EAAA;AACtC,MAAA,OAAO;AAAA,QACL,IAAA;AAAA,QACA,IAAA;AAAA,QACA,IAAA,EAAM,MAAA;AAAA,QACN,IAAA,EAAM,SAAS,aAAA,IAAiB,CAAA;AAAA,QAChC,SAAA,EAAW,QAAA,CAAS,YAAA,oBAAgB,IAAI,IAAA,EAAK;AAAA,QAC7C,UAAA,EAAY,QAAA,CAAS,YAAA,oBAAgB,IAAI,IAAA;AAAK,OAChD;AAAA,IACF,SAAS,KAAA,EAAgB;AACvB,MAAA,IAAI,CAAC,eAAA,CAAgB,KAAK,GAAG,MAAM,IAAA,CAAK,YAAY,KAAK,CAAA;AAEzD,MAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,WAAA,CAAY,IAAI,CAAA;AACzC,MAAA,IAAI,KAAA,EAAO;AACT,QAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,GAAG,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,GAAA,EAAI,IAAK,EAAA;AACtD,QAAA,OAAO;AAAA,UACL,IAAA;AAAA,UACA,IAAA;AAAA,UACA,IAAA,EAAM,WAAA;AAAA,UACN,IAAA,EAAM,CAAA;AAAA,UACN,SAAA,sBAAe,IAAA,EAAK;AAAA,UACpB,UAAA,sBAAgB,IAAA;AAAK,SACvB;AAAA,MACF;AACA,MAAA,MAAM,IAAI,kBAAkB,IAAI,CAAA;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,IAAA,EAAgC;AAC3C,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,IAAA,IAAI,CAAC,KAAK,OAAO,KAAA;AAEjB,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,CAAO,IAAA;AAAA,QACX,IAAI,iBAAA,CAAkB;AAAA,UACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,GAAA,EAAK;AAAA,SACN;AAAA,OACH;AACA,MAAA,OAAO,IAAA;AAAA,IACT,SAAS,KAAA,EAAgB;AACvB,MAAA,IAAI,CAAC,eAAA,CAAgB,KAAK,GAAG,MAAM,IAAA,CAAK,YAAY,KAAK,CAAA;AACzD,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,IAAA,EAAgC;AAChD,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,cAAA,EAAe;AAEzC,IAAA,MAAM,QAAA,GAAqC,MAAM,MAAA,CAAO,IAAA;AAAA,MACtD,IAAI,oBAAA,CAAqB;AAAA,QACvB,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,MAAA,EAAQ,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,GAAI,GAAA;AAAA,QACjC,OAAA,EAAS;AAAA,OACV;AAAA,KACH;AAEA,IAAA,OAAA,CAAQ,QAAA,CAAS,QAAA,EAAU,MAAA,IAAU,CAAA,IAAK,CAAA;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,IAAA,GAAsB;AAE1B,IAAA,MAAM,MAAA,GAAS,KAAK,SAAA,EAAU;AAC9B,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,CAAO,KAAK,IAAI,iBAAA,CAAkB,EAAE,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAQ,CAAC,CAAA;AAAA,IAClE,SAAS,KAAA,EAAO;AAEd,MAAA,MAAM,UAAA,GAAc,MAAsD,SAAA,EAAW,cAAA;AAGrF,MAAA,MAAM,WAAA,GAAc,CAACA,QAAAA,KAAoB;AACvC,QAAA,MAAM,GAAA,GAAM,IAAI,KAAA,CAAMA,QAAO,CAAA;AAC7B,QAAA,IAAI,UAAA,MAAgB,MAAA,GAAS,UAAA;AAC7B,QAAA,OAAO,GAAA;AAAA,MACT,CAAA;AAGA,MAAA,IAAI,mBAAA,CAAoB,KAAK,CAAA,EAAG;AAC9B,QAAA,MAAM,WAAA,CAAY,CAAA,yBAAA,EAA4B,IAAA,CAAK,MAAM,CAAA,qCAAA,CAAuC,CAAA;AAAA,MAClG;AACA,MAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG;AAC1B,QAAA,MAAM,WAAA,CAAY,CAAA,QAAA,EAAW,IAAA,CAAK,MAAM,CAAA,WAAA,CAAa,CAAA;AAAA,MACvD;AACA,MAAA,MAAM,UAAU,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AACrE,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,MAAM,WAAA,CAAY,4BAA4B,IAAA,CAAK,MAAM,WAAW,UAAU,CAAA,GAAA,EAAM,OAAO,CAAA,CAAE,CAAA;AAAA,MAC/F;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAA,GAAyB;AAC7B,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AACF","file":"index.js","sourcesContent":["import {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n CopyObjectCommand,\n ListObjectsV2Command,\n DeleteObjectsCommand,\n HeadObjectCommand,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3';\n\nimport type {\n FileContent,\n FileStat,\n FileEntry,\n ReadOptions,\n WriteOptions,\n ListOptions,\n RemoveOptions,\n CopyOptions,\n FilesystemMountConfig,\n FilesystemIcon,\n FilesystemInfo,\n ProviderStatus,\n} from '@mastra/core/workspace';\nimport { MastraFilesystem, FileNotFoundError } from '@mastra/core/workspace';\n\n/**\n * S3 mount configuration.\n * Returned by S3Filesystem.getMountConfig() for FUSE mounting in sandboxes.\n */\nexport interface S3MountConfig extends FilesystemMountConfig {\n type: 's3';\n /** S3 bucket name */\n bucket: string;\n /** AWS region (use 'auto' for R2) */\n region?: string;\n /** Optional endpoint for S3-compatible storage (MinIO, R2, etc.) */\n endpoint?: string;\n /** AWS access key ID */\n accessKeyId?: string;\n /** AWS secret access key */\n secretAccessKey?: string;\n /** Mount as read-only */\n readOnly?: boolean;\n}\n\n/**\n * Common MIME types by file extension.\n */\nconst MIME_TYPES: Record<string, string> = {\n // Text\n '.txt': 'text/plain',\n '.md': 'text/markdown',\n '.markdown': 'text/markdown',\n '.html': 'text/html',\n '.htm': 'text/html',\n '.css': 'text/css',\n '.csv': 'text/csv',\n '.xml': 'text/xml',\n // Code\n '.js': 'text/javascript',\n '.mjs': 'text/javascript',\n '.ts': 'text/typescript',\n '.tsx': 'text/typescript',\n '.jsx': 'text/javascript',\n '.json': 'application/json',\n '.yaml': 'text/yaml',\n '.yml': 'text/yaml',\n '.py': 'text/x-python',\n '.rb': 'text/x-ruby',\n '.sh': 'text/x-shellscript',\n '.bash': 'text/x-shellscript',\n // Images\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n // Documents\n '.pdf': 'application/pdf',\n // Archives\n '.zip': 'application/zip',\n '.gz': 'application/gzip',\n '.tar': 'application/x-tar',\n};\n\n/**\n * Get MIME type from file path extension.\n */\nfunction getMimeType(path: string): string {\n const ext = path.toLowerCase().match(/\\.[^.]+$/)?.[0];\n return ext ? (MIME_TYPES[ext] ?? 'application/octet-stream') : 'application/octet-stream';\n}\n\n/** Check if an error is a \"not found\" error from the S3 SDK. */\nfunction isNotFoundError(error: unknown): boolean {\n if (!error || typeof error !== 'object' || !('name' in error)) return false;\n const name = (error as { name: string }).name;\n return name === 'NotFound' || name === 'NoSuchKey' || name === '404';\n}\n\n/** Check if an error is an access denied error from the S3 SDK. */\nfunction isAccessDeniedError(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false;\n const err = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n return err.name === 'AccessDenied' || err.$metadata?.httpStatusCode === 403;\n}\n\n/**\n * S3 filesystem provider configuration.\n */\nexport interface S3FilesystemOptions {\n /** Unique identifier for this filesystem instance */\n id?: string;\n /** S3 bucket name */\n bucket: string;\n /** Human-friendly display name for the UI */\n displayName?: string;\n /** Icon identifier for the UI (defaults to 's3') */\n icon?: FilesystemIcon;\n /** Description shown in tooltips */\n description?: string;\n /** AWS region (use 'auto' for R2) */\n region: string;\n /**\n * AWS access key ID.\n * Optional - omit for public buckets (read-only access).\n */\n accessKeyId?: string;\n /**\n * AWS secret access key.\n * Optional - omit for public buckets (read-only access).\n */\n secretAccessKey?: string;\n /**\n * Custom endpoint URL for S3-compatible storage.\n * Examples:\n * - Cloudflare R2: 'https://{accountId}.r2.cloudflarestorage.com'\n * - MinIO: 'http://localhost:9000'\n * - DigitalOcean Spaces: 'https://{region}.digitaloceanspaces.com'\n */\n endpoint?: string;\n /** Force path-style URLs (required for some S3-compatible services) */\n forcePathStyle?: boolean;\n /** Optional prefix for all keys (acts like a subdirectory) */\n prefix?: string;\n /** Mount as read-only (blocks write operations, mounts read-only in sandboxes) */\n readOnly?: boolean;\n}\n\n/**\n * S3 filesystem implementation.\n *\n * Stores files in an S3 bucket or S3-compatible storage service.\n * Supports mounting into E2B sandboxes via s3fs-fuse.\n *\n * @example AWS S3\n * ```typescript\n * import { S3Filesystem } from '@mastra/s3';\n *\n * const fs = new S3Filesystem({\n * bucket: 'my-bucket',\n * region: 'us-east-1',\n * accessKeyId: process.env.AWS_ACCESS_KEY_ID!,\n * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,\n * });\n * ```\n *\n * @example Cloudflare R2\n * ```typescript\n * import { S3Filesystem } from '@mastra/s3';\n *\n * const fs = new S3Filesystem({\n * bucket: 'my-bucket',\n * region: 'auto',\n * accessKeyId: process.env.R2_ACCESS_KEY_ID!,\n * secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,\n * endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,\n * });\n * ```\n *\n * @example MinIO (local)\n * ```typescript\n * import { S3Filesystem } from '@mastra/s3';\n *\n * const fs = new S3Filesystem({\n * bucket: 'my-bucket',\n * region: 'us-east-1',\n * accessKeyId: 'minioadmin',\n * secretAccessKey: 'minioadmin',\n * endpoint: 'http://localhost:9000',\n * forcePathStyle: true,\n * });\n * ```\n */\n\n/** Trim leading and trailing slashes without regex (avoids polynomial regex on user input). */\nfunction trimSlashes(s: string): string {\n let start = 0;\n let end = s.length;\n while (start < end && s[start] === '/') start++;\n while (end > start && s[end - 1] === '/') end--;\n return s.slice(start, end);\n}\n\nexport class S3Filesystem extends MastraFilesystem {\n readonly id: string;\n readonly name = 'S3Filesystem';\n readonly provider = 's3';\n readonly readOnly?: boolean;\n\n status: ProviderStatus = 'pending';\n\n // Display metadata for UI\n readonly displayName?: string;\n readonly icon: FilesystemIcon = 's3';\n readonly description?: string;\n\n private readonly bucket: string;\n private readonly region: string;\n private readonly accessKeyId?: string;\n private readonly secretAccessKey?: string;\n private readonly endpoint?: string;\n private readonly forcePathStyle: boolean;\n private readonly prefix: string;\n\n private _client: S3Client | null = null;\n\n constructor(options: S3FilesystemOptions) {\n super({ name: 'S3Filesystem' });\n this.id = options.id ?? `s3-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n this.bucket = options.bucket;\n this.region = options.region;\n this.accessKeyId = options.accessKeyId;\n this.secretAccessKey = options.secretAccessKey;\n this.endpoint = options.endpoint;\n this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint; // Default true for custom endpoints\n // Trim leading/trailing slashes from prefix using iterative approach (avoids polynomial regex)\n this.prefix = options.prefix ? trimSlashes(options.prefix) + '/' : '';\n\n // Display metadata - detect icon first, then derive displayName from it\n this.icon = options.icon ?? this.detectIconFromEndpoint(options.endpoint);\n this.displayName = options.displayName ?? this.getDefaultDisplayName(this.icon);\n this.description = options.description;\n this.readOnly = options.readOnly;\n }\n\n /**\n * Get mount configuration for E2B sandbox.\n * Returns S3-compatible config that works with s3fs-fuse.\n */\n getMountConfig(): S3MountConfig {\n const config: S3MountConfig = {\n type: 's3',\n bucket: this.bucket,\n region: this.region,\n endpoint: this.endpoint,\n };\n\n if (this.accessKeyId && this.secretAccessKey) {\n config.accessKeyId = this.accessKeyId;\n config.secretAccessKey = this.secretAccessKey;\n }\n\n if (this.readOnly) {\n config.readOnly = true;\n }\n\n return config;\n }\n\n /**\n * Get filesystem info for status reporting.\n */\n getInfo(): FilesystemInfo {\n return {\n id: this.id,\n name: this.name,\n provider: this.provider,\n status: this.status,\n error: this.error,\n icon: this.icon,\n metadata: {\n bucket: this.bucket,\n region: this.region,\n ...(this.endpoint && { endpoint: this.endpoint }),\n ...(this.prefix && { prefix: this.prefix }),\n },\n };\n }\n\n /**\n * Handle an error, checking for access denied and updating status accordingly.\n * Returns the error for re-throwing.\n */\n private handleError(error: unknown): unknown {\n if (isAccessDeniedError(error)) {\n this.status = 'error';\n this.error = 'Access denied - check credentials and bucket permissions';\n }\n return error;\n }\n\n /**\n * Get instructions describing this S3 filesystem.\n * Used by agents to understand storage semantics.\n */\n getInstructions(): string {\n const providerName = this.displayName || 'S3';\n const access = this.readOnly ? 'Read-only' : 'Persistent';\n return `${providerName} storage in bucket \"${this.bucket}\". ${access} storage - files are retained across sessions.`;\n }\n\n /**\n * Detect the appropriate icon based on the S3 endpoint.\n */\n private detectIconFromEndpoint(endpoint?: string): FilesystemIcon {\n if (!endpoint) {\n // No custom endpoint = AWS S3\n return 'aws-s3';\n }\n\n // Parse hostname from endpoint URL for secure matching\n let hostname: string;\n try {\n const url = new URL(endpoint);\n hostname = url.hostname.toLowerCase();\n } catch {\n // If URL parsing fails, use the endpoint as-is (lowercased)\n hostname = endpoint.toLowerCase();\n }\n\n // Check hostname suffix for known providers (use dot-prefix or exact match to prevent subdomain spoofing)\n if (\n hostname === 'r2.cloudflarestorage.com' ||\n hostname.endsWith('.r2.cloudflarestorage.com') ||\n hostname.endsWith('.cloudflare.com')\n ) {\n return 'r2';\n }\n\n if (\n hostname === 'storage.googleapis.com' ||\n hostname.endsWith('.storage.googleapis.com') ||\n hostname.endsWith('.googleapis.com')\n ) {\n return 'gcs';\n }\n\n if (\n hostname === 'blob.core.windows.net' ||\n hostname.endsWith('.blob.core.windows.net') ||\n hostname.endsWith('.azure.com')\n ) {\n return 'azure';\n }\n\n if (hostname.includes('minio')) {\n return 'minio';\n }\n\n // Generic S3-compatible (DigitalOcean Spaces, etc.)\n return 's3';\n }\n\n /**\n * Get a user-friendly display name based on the icon/provider.\n */\n private getDefaultDisplayName(icon: FilesystemIcon): string | undefined {\n switch (icon) {\n case 'aws-s3':\n return 'AWS S3';\n case 'r2':\n case 'cloudflare':\n case 'cloudflare-r2':\n return 'Cloudflare R2';\n case 'gcs':\n case 'google-cloud':\n case 'google-cloud-storage':\n return 'Google Cloud Storage';\n case 'azure':\n case 'azure-blob':\n return 'Azure Blob';\n case 'minio':\n return 'MinIO';\n case 's3':\n return 'S3';\n default:\n // Unknown icon - don't assume a display name\n return undefined;\n }\n }\n\n private getClient(): S3Client {\n if (this._client) return this._client;\n\n const hasCredentials = this.accessKeyId && this.secretAccessKey;\n\n this._client = new S3Client({\n region: this.region,\n credentials: hasCredentials\n ? {\n accessKeyId: this.accessKeyId!,\n secretAccessKey: this.secretAccessKey!,\n }\n : // Anonymous access for public buckets - use empty credentials\n // to prevent SDK from trying to find credentials elsewhere\n { accessKeyId: '', secretAccessKey: '' },\n endpoint: this.endpoint,\n forcePathStyle: this.forcePathStyle,\n // Skip signing for anonymous access (public buckets).\n // No-op signer passes the request through unsigned. Uses `any` because\n // the correct type (HttpRequest from @smithy/types) is not a direct dependency.\n\n ...(hasCredentials ? {} : { signer: { sign: async (request: any) => request } }),\n });\n\n return this._client;\n }\n\n /**\n * Ensure the filesystem is initialized and return the S3 client.\n * Uses base class ensureReady() for status management, then returns client.\n */\n private async getReadyClient(): Promise<S3Client> {\n await this.ensureReady();\n return this.getClient();\n }\n\n private toKey(path: string): string {\n // Remove leading slash and add prefix\n const cleanPath = path.replace(/^\\/+/, '');\n return this.prefix + cleanPath;\n }\n\n // ---------------------------------------------------------------------------\n // File Operations\n // ---------------------------------------------------------------------------\n\n async readFile(path: string, options?: ReadOptions): Promise<string | Buffer> {\n const client = await this.getReadyClient();\n\n try {\n const response = await client.send(\n new GetObjectCommand({\n Bucket: this.bucket,\n Key: this.toKey(path),\n }),\n );\n\n const body = await response.Body?.transformToByteArray();\n if (!body) throw new FileNotFoundError(path);\n\n const buffer = Buffer.from(body);\n if (options?.encoding) {\n return buffer.toString(options.encoding);\n }\n return buffer;\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\n throw new FileNotFoundError(path);\n }\n throw this.handleError(error);\n }\n }\n\n async writeFile(path: string, content: FileContent, _options?: WriteOptions): Promise<void> {\n const client = await this.getReadyClient();\n\n const body = typeof content === 'string' ? Buffer.from(content, 'utf-8') : Buffer.from(content);\n const contentType = getMimeType(path);\n\n await client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: this.toKey(path),\n Body: body,\n ContentType: contentType,\n }),\n );\n }\n\n async appendFile(path: string, content: FileContent): Promise<void> {\n // S3 doesn't support append, so read + write\n let existing = '';\n try {\n existing = (await this.readFile(path, { encoding: 'utf-8' })) as string;\n } catch (error) {\n if (error instanceof FileNotFoundError) {\n // File doesn't exist, start fresh\n } else {\n throw error;\n }\n }\n\n const appendContent = typeof content === 'string' ? content : Buffer.from(content).toString('utf-8');\n await this.writeFile(path, existing + appendContent);\n }\n\n async deleteFile(path: string, options?: RemoveOptions): Promise<void> {\n // Check if this is a directory - if so, use rmdir instead\n const isDir = await this.isDirectory(path);\n if (isDir) {\n await this.rmdir(path, { recursive: true, force: options?.force });\n return;\n }\n\n const client = await this.getReadyClient();\n\n try {\n await client.send(\n new DeleteObjectCommand({\n Bucket: this.bucket,\n Key: this.toKey(path),\n }),\n );\n } catch (error: unknown) {\n if (options?.force) return;\n if (isNotFoundError(error)) {\n throw new FileNotFoundError(path);\n }\n throw this.handleError(error);\n }\n }\n\n async copyFile(src: string, dest: string, _options?: CopyOptions): Promise<void> {\n const client = await this.getReadyClient();\n\n try {\n await client.send(\n new CopyObjectCommand({\n Bucket: this.bucket,\n CopySource: `${this.bucket}/${encodeURIComponent(this.toKey(src)).replace(/%2F/g, '/')}`,\n Key: this.toKey(dest),\n }),\n );\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\n throw new FileNotFoundError(src);\n }\n throw this.handleError(error);\n }\n }\n\n async moveFile(src: string, dest: string, options?: CopyOptions): Promise<void> {\n await this.copyFile(src, dest, options);\n await this.deleteFile(src, { force: true });\n }\n\n // ---------------------------------------------------------------------------\n // Directory Operations\n // ---------------------------------------------------------------------------\n\n async mkdir(_path: string, _options?: { recursive?: boolean }): Promise<void> {\n // S3 doesn't have real directories - they're just key prefixes\n // No-op, directories are created implicitly when files are written\n }\n\n async rmdir(path: string, options?: RemoveOptions): Promise<void> {\n if (!options?.recursive) {\n // Check if directory is empty\n const entries = await this.readdir(path);\n if (entries.length > 0) {\n throw new Error(`Directory not empty: ${path}`);\n }\n return;\n }\n\n // Delete all objects with this prefix\n const client = await this.getReadyClient();\n\n const prefix = this.toKey(path).replace(/\\/$/, '') + '/';\n\n let continuationToken: string | undefined;\n do {\n const listResponse = await client.send(\n new ListObjectsV2Command({\n Bucket: this.bucket,\n Prefix: prefix,\n ContinuationToken: continuationToken,\n }),\n );\n\n if (listResponse.Contents && listResponse.Contents.length > 0) {\n const deleteResponse = await client.send(\n new DeleteObjectsCommand({\n Bucket: this.bucket,\n Delete: {\n Objects: listResponse.Contents.filter((obj): obj is { Key: string } => !!obj.Key).map(obj => ({\n Key: obj.Key,\n })),\n },\n }),\n );\n if (deleteResponse.Errors && deleteResponse.Errors.length > 0) {\n throw new Error(`Failed to delete ${deleteResponse.Errors.length} object(s) in ${path}`);\n }\n }\n\n continuationToken = listResponse.NextContinuationToken;\n } while (continuationToken);\n }\n\n async readdir(path: string, options?: ListOptions): Promise<FileEntry[]> {\n const client = await this.getReadyClient();\n\n const prefix = this.toKey(path).replace(/\\/$/, '');\n const searchPrefix = prefix ? prefix + '/' : '';\n\n const entries: FileEntry[] = [];\n const seenDirs = new Set<string>();\n\n let continuationToken: string | undefined;\n do {\n const response = await client.send(\n new ListObjectsV2Command({\n Bucket: this.bucket,\n Prefix: searchPrefix,\n Delimiter: options?.recursive ? undefined : '/',\n ContinuationToken: continuationToken,\n }),\n );\n\n // Add files\n if (response.Contents) {\n for (const obj of response.Contents) {\n const key = obj.Key;\n if (!key || key === searchPrefix) continue;\n\n const relativePath = key.slice(searchPrefix.length);\n if (!relativePath) continue;\n\n // Skip if this looks like a directory marker\n if (relativePath.endsWith('/')) {\n const dirName = relativePath.slice(0, -1);\n if (!seenDirs.has(dirName)) {\n seenDirs.add(dirName);\n entries.push({ name: dirName, type: 'directory' });\n }\n continue;\n }\n\n const name = options?.recursive ? relativePath : relativePath.split('/')[0];\n\n // Skip if name is undefined or empty\n if (!name) continue;\n\n // Filter by extension if specified\n if (options?.extension) {\n const extensions = Array.isArray(options.extension) ? options.extension : [options.extension];\n if (!extensions.some(ext => name.endsWith(ext))) {\n continue;\n }\n }\n\n entries.push({\n name,\n type: 'file',\n size: obj.Size,\n });\n }\n }\n\n // Add directories (common prefixes)\n if (response.CommonPrefixes) {\n for (const prefixObj of response.CommonPrefixes) {\n if (!prefixObj.Prefix) continue;\n const dirName = prefixObj.Prefix.slice(searchPrefix.length).replace(/\\/$/, '');\n if (dirName && !seenDirs.has(dirName)) {\n seenDirs.add(dirName);\n entries.push({ name: dirName, type: 'directory' });\n }\n }\n }\n\n continuationToken = response.NextContinuationToken;\n } while (continuationToken);\n\n return entries;\n }\n\n // ---------------------------------------------------------------------------\n // Path Operations\n // ---------------------------------------------------------------------------\n\n async exists(path: string): Promise<boolean> {\n const key = this.toKey(path);\n if (!key) return true; // Root always exists\n\n const client = await this.getReadyClient();\n\n // Check if it's a file\n try {\n await client.send(\n new HeadObjectCommand({\n Bucket: this.bucket,\n Key: key,\n }),\n );\n return true;\n } catch (error: unknown) {\n if (!isNotFoundError(error)) throw this.handleError(error);\n // Not a file, check if it's a \"directory\" (has objects with this prefix)\n }\n\n // Check if it's a directory prefix\n const response: { Contents?: unknown[] } = await client.send(\n new ListObjectsV2Command({\n Bucket: this.bucket,\n Prefix: key.replace(/\\/$/, '') + '/',\n MaxKeys: 1,\n }),\n );\n\n return (response.Contents?.length ?? 0) > 0;\n }\n\n async stat(path: string): Promise<FileStat> {\n const key = this.toKey(path);\n\n // Root is always a directory\n if (!key) {\n return {\n name: '',\n path,\n type: 'directory',\n size: 0,\n createdAt: new Date(),\n modifiedAt: new Date(),\n };\n }\n\n const client = await this.getReadyClient();\n\n try {\n const response: { ContentLength?: number; LastModified?: Date } = await client.send(\n new HeadObjectCommand({\n Bucket: this.bucket,\n Key: key,\n }),\n );\n\n const name = path.split('/').pop() ?? '';\n return {\n name,\n path,\n type: 'file',\n size: response.ContentLength ?? 0,\n createdAt: response.LastModified ?? new Date(),\n modifiedAt: response.LastModified ?? new Date(),\n };\n } catch (error: unknown) {\n if (!isNotFoundError(error)) throw this.handleError(error);\n // Check if it's a directory\n const isDir = await this.isDirectory(path);\n if (isDir) {\n const name = path.split('/').filter(Boolean).pop() ?? '';\n return {\n name,\n path,\n type: 'directory',\n size: 0,\n createdAt: new Date(),\n modifiedAt: new Date(),\n };\n }\n throw new FileNotFoundError(path);\n }\n }\n\n async isFile(path: string): Promise<boolean> {\n const key = this.toKey(path);\n if (!key) return false; // Root is a directory, not a file\n\n const client = await this.getReadyClient();\n\n try {\n await client.send(\n new HeadObjectCommand({\n Bucket: this.bucket,\n Key: key,\n }),\n );\n return true;\n } catch (error: unknown) {\n if (!isNotFoundError(error)) throw this.handleError(error);\n return false;\n }\n }\n\n async isDirectory(path: string): Promise<boolean> {\n const key = this.toKey(path);\n if (!key) return true; // Root is always a directory\n\n const client = await this.getReadyClient();\n\n const response: { Contents?: unknown[] } = await client.send(\n new ListObjectsV2Command({\n Bucket: this.bucket,\n Prefix: key.replace(/\\/$/, '') + '/',\n MaxKeys: 1,\n }),\n );\n\n return (response.Contents?.length ?? 0) > 0;\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle (overrides base class protected methods)\n // ---------------------------------------------------------------------------\n\n /**\n * Initialize the S3 client.\n * Status management is handled by the base class.\n */\n async init(): Promise<void> {\n // Verify we can access the bucket\n const client = this.getClient();\n try {\n await client.send(new HeadBucketCommand({ Bucket: this.bucket }));\n } catch (error) {\n // Extract httpStatusCode if available\n const statusCode = (error as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode;\n\n // Create error with status property for proper HTTP response codes\n const createError = (message: string) => {\n const err = new Error(message) as Error & { status?: number };\n if (statusCode) err.status = statusCode;\n return err;\n };\n\n // Provide better error messages for common S3 errors\n if (isAccessDeniedError(error)) {\n throw createError(`Access denied to bucket \"${this.bucket}\" - check credentials and permissions`);\n }\n if (isNotFoundError(error)) {\n throw createError(`Bucket \"${this.bucket}\" not found`);\n }\n const message = error instanceof Error ? error.message : String(error);\n if (statusCode) {\n throw createError(`Failed to access bucket \"${this.bucket}\" (HTTP ${statusCode}): ${message}`);\n }\n throw error;\n }\n }\n\n /**\n * Clean up the S3 client.\n * Status management is handled by the base class.\n */\n async destroy(): Promise<void> {\n this._client = null;\n }\n}\n"]}