@mastra/s3 0.6.0 → 0.6.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,844 +1,877 @@
1
- import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, CopyObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, HeadObjectCommand, HeadBucketCommand } from '@aws-sdk/client-s3';
2
- import { MastraFilesystem, FileNotFoundError, FileExistsError } from '@mastra/core/workspace';
3
- import { BlobStore } from '@mastra/core/storage';
4
-
5
- // src/filesystem/index.ts
6
- var MIME_TYPES = {
7
- // Text
8
- ".txt": "text/plain",
9
- ".md": "text/markdown",
10
- ".markdown": "text/markdown",
11
- ".html": "text/html",
12
- ".htm": "text/html",
13
- ".css": "text/css",
14
- ".csv": "text/csv",
15
- ".xml": "text/xml",
16
- // Code
17
- ".js": "text/javascript",
18
- ".mjs": "text/javascript",
19
- ".ts": "text/typescript",
20
- ".tsx": "text/typescript",
21
- ".jsx": "text/javascript",
22
- ".json": "application/json",
23
- ".yaml": "text/yaml",
24
- ".yml": "text/yaml",
25
- ".py": "text/x-python",
26
- ".rb": "text/x-ruby",
27
- ".sh": "text/x-shellscript",
28
- ".bash": "text/x-shellscript",
29
- // Images
30
- ".png": "image/png",
31
- ".jpg": "image/jpeg",
32
- ".jpeg": "image/jpeg",
33
- ".gif": "image/gif",
34
- ".svg": "image/svg+xml",
35
- ".webp": "image/webp",
36
- ".ico": "image/x-icon",
37
- // Documents
38
- ".pdf": "application/pdf",
39
- // Archives
40
- ".zip": "application/zip",
41
- ".gz": "application/gzip",
42
- ".tar": "application/x-tar"
1
+ import { CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
2
+ import { FileExistsError, FileNotFoundError, MastraFilesystem } from "@mastra/core/workspace";
3
+ import { BlobStore } from "@mastra/core/storage";
4
+ //#region src/filesystem/index.ts
5
+ /**
6
+ * Common MIME types by file extension.
7
+ */
8
+ const MIME_TYPES = {
9
+ ".txt": "text/plain",
10
+ ".md": "text/markdown",
11
+ ".markdown": "text/markdown",
12
+ ".html": "text/html",
13
+ ".htm": "text/html",
14
+ ".css": "text/css",
15
+ ".csv": "text/csv",
16
+ ".xml": "text/xml",
17
+ ".js": "text/javascript",
18
+ ".mjs": "text/javascript",
19
+ ".ts": "text/typescript",
20
+ ".tsx": "text/typescript",
21
+ ".jsx": "text/javascript",
22
+ ".json": "application/json",
23
+ ".yaml": "text/yaml",
24
+ ".yml": "text/yaml",
25
+ ".py": "text/x-python",
26
+ ".rb": "text/x-ruby",
27
+ ".sh": "text/x-shellscript",
28
+ ".bash": "text/x-shellscript",
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
+ ".pdf": "application/pdf",
37
+ ".zip": "application/zip",
38
+ ".gz": "application/gzip",
39
+ ".tar": "application/x-tar"
43
40
  };
41
+ /**
42
+ * Get MIME type from file path extension.
43
+ */
44
44
  function getMimeType(path) {
45
- const ext = path.toLowerCase().match(/\.[^.]+$/)?.[0];
46
- return ext ? MIME_TYPES[ext] ?? "application/octet-stream" : "application/octet-stream";
45
+ const ext = path.toLowerCase().match(/\.[^.]+$/)?.[0];
46
+ return ext ? MIME_TYPES[ext] ?? "application/octet-stream" : "application/octet-stream";
47
47
  }
48
- function isNotFoundError(error) {
49
- if (!error || typeof error !== "object" || !("name" in error)) return false;
50
- const name = error.name;
51
- return name === "NotFound" || name === "NoSuchKey" || name === "404";
48
+ /** Check if an error is a "not found" error from the S3 SDK. */
49
+ function isNotFoundError$1(error) {
50
+ if (!error || typeof error !== "object" || !("name" in error)) return false;
51
+ const name = error.name;
52
+ return name === "NotFound" || name === "NoSuchKey" || name === "404";
52
53
  }
54
+ /** Check if an error is an access denied error from the S3 SDK. */
53
55
  function isAccessDeniedError(error) {
54
- if (!error || typeof error !== "object") return false;
55
- const err = error;
56
- return err.name === "AccessDenied" || err.$metadata?.httpStatusCode === 403;
56
+ if (!error || typeof error !== "object") return false;
57
+ const err = error;
58
+ return err.name === "AccessDenied" || err.$metadata?.httpStatusCode === 403;
57
59
  }
58
- function trimSlashes(s) {
59
- let start = 0;
60
- let end = s.length;
61
- while (start < end && s[start] === "/") start++;
62
- while (end > start && s[end - 1] === "/") end--;
63
- return s.slice(start, end);
60
+ /**
61
+ * S3 filesystem implementation.
62
+ *
63
+ * Stores files in an S3 bucket or S3-compatible storage service.
64
+ * Supports mounting into E2B sandboxes via s3fs-fuse.
65
+ *
66
+ * @example AWS S3
67
+ * ```typescript
68
+ * import { S3Filesystem } from '@mastra/s3';
69
+ *
70
+ * const fs = new S3Filesystem({
71
+ * bucket: 'my-bucket',
72
+ * region: 'us-east-1',
73
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
74
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
75
+ * });
76
+ * ```
77
+ *
78
+ * @example Cloudflare R2
79
+ * ```typescript
80
+ * import { S3Filesystem } from '@mastra/s3';
81
+ *
82
+ * const fs = new S3Filesystem({
83
+ * bucket: 'my-bucket',
84
+ * region: 'auto',
85
+ * accessKeyId: process.env.R2_ACCESS_KEY_ID!,
86
+ * secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
87
+ * endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
88
+ * });
89
+ * ```
90
+ *
91
+ * @example MinIO (local)
92
+ * ```typescript
93
+ * import { S3Filesystem } from '@mastra/s3';
94
+ *
95
+ * const fs = new S3Filesystem({
96
+ * bucket: 'my-bucket',
97
+ * region: 'us-east-1',
98
+ * accessKeyId: 'minioadmin',
99
+ * secretAccessKey: 'minioadmin',
100
+ * endpoint: 'http://localhost:9000',
101
+ * forcePathStyle: true,
102
+ * });
103
+ * ```
104
+ */
105
+ /** Trim leading and trailing slashes without regex (avoids polynomial regex on user input). */
106
+ function trimSlashes$1(s) {
107
+ let start = 0;
108
+ let end = s.length;
109
+ while (start < end && s[start] === "/") start++;
110
+ while (end > start && s[end - 1] === "/") end--;
111
+ return s.slice(start, end);
64
112
  }
65
113
  var S3Filesystem = class extends MastraFilesystem {
66
- id;
67
- name = "S3Filesystem";
68
- provider = "s3";
69
- readOnly;
70
- status = "pending";
71
- // Display metadata for UI
72
- displayName;
73
- icon = "s3";
74
- description;
75
- bucket;
76
- region;
77
- credentials;
78
- accessKeyId;
79
- secretAccessKey;
80
- sessionToken;
81
- endpoint;
82
- forcePathStyle;
83
- prefix;
84
- _client = null;
85
- constructor(options) {
86
- super({ ...options, name: "S3Filesystem" });
87
- this.id = options.id ?? `s3-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
88
- this.bucket = options.bucket;
89
- this.region = options.region;
90
- this.credentials = options.credentials;
91
- this.accessKeyId = options.accessKeyId;
92
- this.secretAccessKey = options.secretAccessKey;
93
- this.sessionToken = options.sessionToken;
94
- this.endpoint = options.endpoint;
95
- this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
96
- const trimmedPrefix = options.prefix ? trimSlashes(options.prefix) : "";
97
- this.prefix = trimmedPrefix ? trimmedPrefix + "/" : "";
98
- this.icon = options.icon ?? this.detectIconFromEndpoint(options.endpoint);
99
- this.displayName = options.displayName ?? this.getDefaultDisplayName(this.icon);
100
- this.description = options.description;
101
- this.readOnly = options.readOnly;
102
- }
103
- /**
104
- * Get the underlying S3Client instance for direct access to AWS S3 APIs.
105
- *
106
- * Use this when you need to access S3 features not exposed through the
107
- * WorkspaceFilesystem interface (e.g., presigned URLs, multipart uploads,
108
- * custom S3 operations, etc.).
109
- *
110
- * @example Generate a presigned URL
111
- * ```typescript
112
- * import { GetObjectCommand } from '@aws-sdk/client-s3';
113
- * import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
114
- *
115
- * const s3Client = fs.client;
116
- * const url = await getSignedUrl(s3Client, new GetObjectCommand({
117
- * Bucket: 'my-bucket',
118
- * Key: 'my-file.txt',
119
- * }));
120
- * ```
121
- */
122
- get client() {
123
- return this.getClient();
124
- }
125
- /**
126
- * Get mount configuration for E2B sandbox.
127
- * Returns S3-compatible config that works with s3fs-fuse.
128
- *
129
- * Only static `accessKeyId`/`secretAccessKey`/`sessionToken` are included in the
130
- * returned config. If credentials are provided only via the `credentials` option
131
- * (provider function), the returned config will have no credentials because FUSE
132
- * mounts cannot call a provider function. Use static credentials for sandbox
133
- * mount compatibility.
134
- */
135
- getMountConfig() {
136
- const config = {
137
- type: "s3",
138
- bucket: this.bucket,
139
- region: this.region,
140
- endpoint: this.endpoint
141
- };
142
- if (this.accessKeyId && this.secretAccessKey) {
143
- config.accessKeyId = this.accessKeyId;
144
- config.secretAccessKey = this.secretAccessKey;
145
- if (this.sessionToken) {
146
- config.sessionToken = this.sessionToken;
147
- }
148
- }
149
- if (this.prefix) {
150
- config.prefix = this.prefix;
151
- }
152
- if (this.readOnly) {
153
- config.readOnly = true;
154
- }
155
- return config;
156
- }
157
- /**
158
- * Get filesystem info for status reporting.
159
- */
160
- getInfo() {
161
- return {
162
- id: this.id,
163
- name: this.name,
164
- provider: this.provider,
165
- status: this.status,
166
- error: this.error,
167
- readOnly: this.readOnly,
168
- icon: this.icon,
169
- metadata: {
170
- bucket: this.bucket,
171
- region: this.region,
172
- ...this.endpoint && { endpoint: this.endpoint },
173
- ...this.prefix && { prefix: this.prefix }
174
- }
175
- };
176
- }
177
- /**
178
- * Handle an error, checking for access denied and updating status accordingly.
179
- * Returns the error for re-throwing.
180
- */
181
- handleError(error) {
182
- if (isAccessDeniedError(error)) {
183
- this.status = "error";
184
- this.error = "Access denied - check credentials and bucket permissions";
185
- }
186
- return error;
187
- }
188
- /**
189
- * Get instructions describing this S3 filesystem.
190
- * Used by agents to understand storage semantics.
191
- */
192
- getInstructions() {
193
- const providerName = this.displayName || "S3";
194
- const access = this.readOnly ? "Read-only" : "Persistent";
195
- return `${providerName} storage in bucket "${this.bucket}". ${access} storage - files are retained across sessions.`;
196
- }
197
- /**
198
- * Detect the appropriate icon based on the S3 endpoint.
199
- */
200
- detectIconFromEndpoint(endpoint) {
201
- if (!endpoint) {
202
- return "aws-s3";
203
- }
204
- let hostname;
205
- try {
206
- const url = new URL(endpoint);
207
- hostname = url.hostname.toLowerCase();
208
- } catch {
209
- hostname = endpoint.toLowerCase();
210
- }
211
- if (hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com") || hostname.endsWith(".cloudflare.com")) {
212
- return "r2";
213
- }
214
- if (hostname === "storage.googleapis.com" || hostname.endsWith(".storage.googleapis.com") || hostname.endsWith(".googleapis.com")) {
215
- return "gcs";
216
- }
217
- if (hostname === "blob.core.windows.net" || hostname.endsWith(".blob.core.windows.net") || hostname.endsWith(".azure.com")) {
218
- return "azure";
219
- }
220
- if (hostname.includes("minio")) {
221
- return "minio";
222
- }
223
- return "s3";
224
- }
225
- /**
226
- * Get a user-friendly display name based on the icon/provider.
227
- */
228
- getDefaultDisplayName(icon) {
229
- switch (icon) {
230
- case "aws-s3":
231
- return "AWS S3";
232
- case "r2":
233
- case "cloudflare":
234
- case "cloudflare-r2":
235
- return "Cloudflare R2";
236
- case "gcs":
237
- case "google-cloud":
238
- case "google-cloud-storage":
239
- return "Google Cloud Storage";
240
- case "azure":
241
- case "azure-blob":
242
- return "Azure Blob";
243
- case "minio":
244
- return "MinIO";
245
- case "s3":
246
- return "S3";
247
- default:
248
- return void 0;
249
- }
250
- }
251
- getClient() {
252
- if (this._client) return this._client;
253
- const hasStaticCredentials = this.accessKeyId && this.secretAccessKey;
254
- let credentials;
255
- if (this.credentials) {
256
- credentials = this.credentials;
257
- } else if (hasStaticCredentials) {
258
- credentials = {
259
- accessKeyId: this.accessKeyId,
260
- secretAccessKey: this.secretAccessKey,
261
- ...this.sessionToken && { sessionToken: this.sessionToken }
262
- };
263
- }
264
- this._client = new S3Client({
265
- region: this.region,
266
- ...credentials !== void 0 && { credentials },
267
- endpoint: this.endpoint,
268
- forcePathStyle: this.forcePathStyle
269
- });
270
- return this._client;
271
- }
272
- /**
273
- * Ensure the filesystem is initialized and return the S3 client.
274
- * Uses base class ensureReady() for status management, then returns client.
275
- */
276
- async getReadyClient() {
277
- await this.ensureReady();
278
- return this.getClient();
279
- }
280
- toKey(path) {
281
- const cleanPath = path.replace(/^\/+/, "").replace(/^\.(?:\/|$)/, "");
282
- return this.prefix + cleanPath;
283
- }
284
- // ---------------------------------------------------------------------------
285
- // File Operations
286
- // ---------------------------------------------------------------------------
287
- async readFile(path, options) {
288
- const client = await this.getReadyClient();
289
- try {
290
- const response = await client.send(
291
- new GetObjectCommand({
292
- Bucket: this.bucket,
293
- Key: this.toKey(path)
294
- })
295
- );
296
- const body = await response.Body?.transformToByteArray();
297
- if (!body) throw new FileNotFoundError(path);
298
- const buffer = Buffer.from(body);
299
- if (options?.encoding) {
300
- return buffer.toString(options.encoding);
301
- }
302
- return buffer;
303
- } catch (error) {
304
- if (isNotFoundError(error)) {
305
- throw new FileNotFoundError(path);
306
- }
307
- throw this.handleError(error);
308
- }
309
- }
310
- async writeFile(path, content, options) {
311
- const client = await this.getReadyClient();
312
- if (options?.overwrite === false && await this.exists(path)) {
313
- throw new FileExistsError(path);
314
- }
315
- const body = typeof content === "string" ? Buffer.from(content, "utf-8") : Buffer.from(content);
316
- const contentType = getMimeType(path);
317
- await client.send(
318
- new PutObjectCommand({
319
- Bucket: this.bucket,
320
- Key: this.toKey(path),
321
- Body: body,
322
- ContentType: contentType
323
- })
324
- );
325
- }
326
- async appendFile(path, content) {
327
- let existing = "";
328
- try {
329
- existing = await this.readFile(path, { encoding: "utf-8" });
330
- } catch (error) {
331
- if (error instanceof FileNotFoundError) ; else {
332
- throw error;
333
- }
334
- }
335
- const appendContent = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
336
- await this.writeFile(path, existing + appendContent);
337
- }
338
- async deleteFile(path, options) {
339
- const isDir = await this.isDirectory(path);
340
- if (isDir) {
341
- await this.rmdir(path, { recursive: true, force: options?.force });
342
- return;
343
- }
344
- const client = await this.getReadyClient();
345
- try {
346
- await client.send(
347
- new DeleteObjectCommand({
348
- Bucket: this.bucket,
349
- Key: this.toKey(path)
350
- })
351
- );
352
- } catch (error) {
353
- if (options?.force) return;
354
- if (isNotFoundError(error)) {
355
- throw new FileNotFoundError(path);
356
- }
357
- throw this.handleError(error);
358
- }
359
- }
360
- async copyFile(src, dest, options) {
361
- const client = await this.getReadyClient();
362
- if (options?.overwrite === false && await this.exists(dest)) {
363
- throw new FileExistsError(dest);
364
- }
365
- try {
366
- await client.send(
367
- new CopyObjectCommand({
368
- Bucket: this.bucket,
369
- CopySource: `${this.bucket}/${encodeURIComponent(this.toKey(src)).replace(/%2F/g, "/")}`,
370
- Key: this.toKey(dest)
371
- })
372
- );
373
- } catch (error) {
374
- if (isNotFoundError(error)) {
375
- throw new FileNotFoundError(src);
376
- }
377
- throw this.handleError(error);
378
- }
379
- }
380
- async moveFile(src, dest, options) {
381
- await this.copyFile(src, dest, options);
382
- await this.deleteFile(src, { force: true });
383
- }
384
- // ---------------------------------------------------------------------------
385
- // Directory Operations
386
- // ---------------------------------------------------------------------------
387
- async mkdir(_path, _options) {
388
- }
389
- async rmdir(path, options) {
390
- if (!options?.recursive) {
391
- const entries = await this.readdir(path);
392
- if (entries.length > 0) {
393
- throw new Error(`Directory not empty: ${path}`);
394
- }
395
- return;
396
- }
397
- const client = await this.getReadyClient();
398
- const prefix = this.toKey(path).replace(/\/$/, "") + "/";
399
- let continuationToken;
400
- do {
401
- const listResponse = await client.send(
402
- new ListObjectsV2Command({
403
- Bucket: this.bucket,
404
- Prefix: prefix,
405
- ContinuationToken: continuationToken
406
- })
407
- );
408
- if (listResponse.Contents && listResponse.Contents.length > 0) {
409
- const deleteResponse = await client.send(
410
- new DeleteObjectsCommand({
411
- Bucket: this.bucket,
412
- Delete: {
413
- Objects: listResponse.Contents.filter((obj) => !!obj.Key).map((obj) => ({
414
- Key: obj.Key
415
- }))
416
- }
417
- })
418
- );
419
- if (deleteResponse.Errors && deleteResponse.Errors.length > 0) {
420
- throw new Error(`Failed to delete ${deleteResponse.Errors.length} object(s) in ${path}`);
421
- }
422
- }
423
- continuationToken = listResponse.NextContinuationToken;
424
- } while (continuationToken);
425
- }
426
- async readdir(path, options) {
427
- const client = await this.getReadyClient();
428
- const prefix = this.toKey(path).replace(/\/$/, "");
429
- const searchPrefix = prefix ? prefix + "/" : "";
430
- const entries = [];
431
- const seenDirs = /* @__PURE__ */ new Set();
432
- let continuationToken;
433
- do {
434
- const response = await client.send(
435
- new ListObjectsV2Command({
436
- Bucket: this.bucket,
437
- Prefix: searchPrefix,
438
- Delimiter: options?.recursive ? void 0 : "/",
439
- ContinuationToken: continuationToken
440
- })
441
- );
442
- if (response.Contents) {
443
- for (const obj of response.Contents) {
444
- const key = obj.Key;
445
- if (!key || key === searchPrefix) continue;
446
- const relativePath = key.slice(searchPrefix.length);
447
- if (!relativePath) continue;
448
- if (relativePath.endsWith("/")) {
449
- const dirName = relativePath.slice(0, -1);
450
- if (!seenDirs.has(dirName)) {
451
- seenDirs.add(dirName);
452
- entries.push({ name: dirName, type: "directory" });
453
- }
454
- continue;
455
- }
456
- const name = options?.recursive ? relativePath : relativePath.split("/")[0];
457
- if (!name) continue;
458
- if (options?.extension) {
459
- const extensions = Array.isArray(options.extension) ? options.extension : [options.extension];
460
- if (!extensions.some((ext) => name.endsWith(ext))) {
461
- continue;
462
- }
463
- }
464
- entries.push({
465
- name,
466
- type: "file",
467
- size: obj.Size
468
- });
469
- }
470
- }
471
- if (response.CommonPrefixes) {
472
- for (const prefixObj of response.CommonPrefixes) {
473
- if (!prefixObj.Prefix) continue;
474
- const dirName = prefixObj.Prefix.slice(searchPrefix.length).replace(/\/$/, "");
475
- if (dirName && !seenDirs.has(dirName)) {
476
- seenDirs.add(dirName);
477
- entries.push({ name: dirName, type: "directory" });
478
- }
479
- }
480
- }
481
- continuationToken = response.NextContinuationToken;
482
- } while (continuationToken);
483
- return entries;
484
- }
485
- // ---------------------------------------------------------------------------
486
- // Path Operations
487
- // ---------------------------------------------------------------------------
488
- async exists(path) {
489
- const key = this.toKey(path);
490
- if (!key) return true;
491
- const client = await this.getReadyClient();
492
- try {
493
- await client.send(
494
- new HeadObjectCommand({
495
- Bucket: this.bucket,
496
- Key: key
497
- })
498
- );
499
- return true;
500
- } catch (error) {
501
- if (!isNotFoundError(error)) throw this.handleError(error);
502
- }
503
- const response = await client.send(
504
- new ListObjectsV2Command({
505
- Bucket: this.bucket,
506
- Prefix: key.replace(/\/$/, "") + "/",
507
- MaxKeys: 1
508
- })
509
- );
510
- return (response.Contents?.length ?? 0) > 0;
511
- }
512
- async stat(path) {
513
- const key = this.toKey(path);
514
- if (!key) {
515
- return {
516
- name: "",
517
- path,
518
- type: "directory",
519
- size: 0,
520
- createdAt: /* @__PURE__ */ new Date(),
521
- modifiedAt: /* @__PURE__ */ new Date()
522
- };
523
- }
524
- const client = await this.getReadyClient();
525
- try {
526
- const response = await client.send(
527
- new HeadObjectCommand({
528
- Bucket: this.bucket,
529
- Key: key
530
- })
531
- );
532
- const name = path.split("/").pop() ?? "";
533
- return {
534
- name,
535
- path,
536
- type: "file",
537
- size: response.ContentLength ?? 0,
538
- createdAt: response.LastModified ?? /* @__PURE__ */ new Date(),
539
- modifiedAt: response.LastModified ?? /* @__PURE__ */ new Date()
540
- };
541
- } catch (error) {
542
- if (!isNotFoundError(error)) throw this.handleError(error);
543
- const isDir = await this.isDirectory(path);
544
- if (isDir) {
545
- const name = path.split("/").filter(Boolean).pop() ?? "";
546
- return {
547
- name,
548
- path,
549
- type: "directory",
550
- size: 0,
551
- createdAt: /* @__PURE__ */ new Date(),
552
- modifiedAt: /* @__PURE__ */ new Date()
553
- };
554
- }
555
- throw new FileNotFoundError(path);
556
- }
557
- }
558
- async isFile(path) {
559
- const key = this.toKey(path);
560
- if (!key) return false;
561
- const client = await this.getReadyClient();
562
- try {
563
- await client.send(
564
- new HeadObjectCommand({
565
- Bucket: this.bucket,
566
- Key: key
567
- })
568
- );
569
- return true;
570
- } catch (error) {
571
- if (!isNotFoundError(error)) throw this.handleError(error);
572
- return false;
573
- }
574
- }
575
- async isDirectory(path) {
576
- const key = this.toKey(path);
577
- if (!key) return true;
578
- const client = await this.getReadyClient();
579
- const response = await client.send(
580
- new ListObjectsV2Command({
581
- Bucket: this.bucket,
582
- Prefix: key.replace(/\/$/, "") + "/",
583
- MaxKeys: 1
584
- })
585
- );
586
- return (response.Contents?.length ?? 0) > 0;
587
- }
588
- // ---------------------------------------------------------------------------
589
- // Lifecycle (overrides base class protected methods)
590
- // ---------------------------------------------------------------------------
591
- /**
592
- * Initialize the S3 client.
593
- * Status management is handled by the base class.
594
- */
595
- async init() {
596
- const client = this.getClient();
597
- try {
598
- await client.send(new HeadBucketCommand({ Bucket: this.bucket }));
599
- } catch (error) {
600
- const statusCode = error.$metadata?.httpStatusCode;
601
- const createError = (message2) => {
602
- const err = new Error(message2);
603
- if (statusCode) err.status = statusCode;
604
- return err;
605
- };
606
- if (isAccessDeniedError(error)) {
607
- throw createError(`Access denied to bucket "${this.bucket}" - check credentials and permissions`);
608
- }
609
- if (isNotFoundError(error)) {
610
- throw createError(`Bucket "${this.bucket}" not found`);
611
- }
612
- const message = error instanceof Error ? error.message : String(error);
613
- if (statusCode) {
614
- throw createError(`Failed to access bucket "${this.bucket}" (HTTP ${statusCode}): ${message}`);
615
- }
616
- throw error;
617
- }
618
- }
619
- /**
620
- * Clean up the S3 client.
621
- * Status management is handled by the base class.
622
- */
623
- async destroy() {
624
- this._client = null;
625
- }
114
+ id;
115
+ name = "S3Filesystem";
116
+ provider = "s3";
117
+ readOnly;
118
+ status = "pending";
119
+ displayName;
120
+ icon = "s3";
121
+ description;
122
+ bucket;
123
+ region;
124
+ credentials;
125
+ accessKeyId;
126
+ secretAccessKey;
127
+ sessionToken;
128
+ endpoint;
129
+ forcePathStyle;
130
+ prefix;
131
+ _client = null;
132
+ constructor(options) {
133
+ super({
134
+ ...options,
135
+ name: "S3Filesystem"
136
+ });
137
+ this.id = options.id ?? `s3-fs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
138
+ this.bucket = options.bucket;
139
+ this.region = options.region;
140
+ this.credentials = options.credentials;
141
+ this.accessKeyId = options.accessKeyId;
142
+ this.secretAccessKey = options.secretAccessKey;
143
+ this.sessionToken = options.sessionToken;
144
+ this.endpoint = options.endpoint;
145
+ this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
146
+ const trimmedPrefix = options.prefix ? trimSlashes$1(options.prefix) : "";
147
+ this.prefix = trimmedPrefix ? trimmedPrefix + "/" : "";
148
+ this.icon = options.icon ?? this.detectIconFromEndpoint(options.endpoint);
149
+ this.displayName = options.displayName ?? this.getDefaultDisplayName(this.icon);
150
+ this.description = options.description;
151
+ this.readOnly = options.readOnly;
152
+ }
153
+ /**
154
+ * Get the underlying S3Client instance for direct access to AWS S3 APIs.
155
+ *
156
+ * Use this when you need to access S3 features not exposed through the
157
+ * WorkspaceFilesystem interface (e.g., presigned URLs, multipart uploads,
158
+ * custom S3 operations, etc.).
159
+ *
160
+ * @example Generate a presigned URL
161
+ * ```typescript
162
+ * import { GetObjectCommand } from '@aws-sdk/client-s3';
163
+ * import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
164
+ *
165
+ * const s3Client = fs.client;
166
+ * const url = await getSignedUrl(s3Client, new GetObjectCommand({
167
+ * Bucket: 'my-bucket',
168
+ * Key: 'my-file.txt',
169
+ * }));
170
+ * ```
171
+ */
172
+ get client() {
173
+ return this.getClient();
174
+ }
175
+ /**
176
+ * Get mount configuration for E2B sandbox.
177
+ * Returns S3-compatible config that works with s3fs-fuse.
178
+ *
179
+ * Only static `accessKeyId`/`secretAccessKey`/`sessionToken` are included in the
180
+ * returned config. If credentials are provided only via the `credentials` option
181
+ * (provider function), the returned config will have no credentials because FUSE
182
+ * mounts cannot call a provider function. Use static credentials for sandbox
183
+ * mount compatibility.
184
+ */
185
+ getMountConfig() {
186
+ const config = {
187
+ type: "s3",
188
+ bucket: this.bucket,
189
+ region: this.region,
190
+ endpoint: this.endpoint
191
+ };
192
+ if (this.accessKeyId && this.secretAccessKey) {
193
+ config.accessKeyId = this.accessKeyId;
194
+ config.secretAccessKey = this.secretAccessKey;
195
+ if (this.sessionToken) config.sessionToken = this.sessionToken;
196
+ }
197
+ if (this.prefix) config.prefix = this.prefix;
198
+ if (this.readOnly) config.readOnly = true;
199
+ return config;
200
+ }
201
+ /**
202
+ * Get filesystem info for status reporting.
203
+ */
204
+ getInfo() {
205
+ return {
206
+ id: this.id,
207
+ name: this.name,
208
+ provider: this.provider,
209
+ status: this.status,
210
+ error: this.error,
211
+ readOnly: this.readOnly,
212
+ icon: this.icon,
213
+ metadata: {
214
+ bucket: this.bucket,
215
+ region: this.region,
216
+ ...this.endpoint && { endpoint: this.endpoint },
217
+ ...this.prefix && { prefix: this.prefix }
218
+ }
219
+ };
220
+ }
221
+ /**
222
+ * Handle an error, checking for access denied and updating status accordingly.
223
+ * Returns the error for re-throwing.
224
+ */
225
+ handleError(error) {
226
+ if (isAccessDeniedError(error)) {
227
+ this.status = "error";
228
+ this.error = "Access denied - check credentials and bucket permissions";
229
+ }
230
+ return error;
231
+ }
232
+ /**
233
+ * Get instructions describing this S3 filesystem.
234
+ * Used by agents to understand storage semantics.
235
+ */
236
+ getInstructions() {
237
+ const providerName = this.displayName || "S3";
238
+ const access = this.readOnly ? "Read-only" : "Persistent";
239
+ return `${providerName} storage in bucket "${this.bucket}". ${access} storage - files are retained across sessions.`;
240
+ }
241
+ /**
242
+ * Detect the appropriate icon based on the S3 endpoint.
243
+ */
244
+ detectIconFromEndpoint(endpoint) {
245
+ if (!endpoint) return "aws-s3";
246
+ let hostname;
247
+ try {
248
+ hostname = new URL(endpoint).hostname.toLowerCase();
249
+ } catch {
250
+ hostname = endpoint.toLowerCase();
251
+ }
252
+ if (hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com") || hostname.endsWith(".cloudflare.com")) return "r2";
253
+ if (hostname === "storage.googleapis.com" || hostname.endsWith(".storage.googleapis.com") || hostname.endsWith(".googleapis.com")) return "gcs";
254
+ if (hostname === "blob.core.windows.net" || hostname.endsWith(".blob.core.windows.net") || hostname.endsWith(".azure.com")) return "azure";
255
+ if (hostname.includes("minio")) return "minio";
256
+ return "s3";
257
+ }
258
+ /**
259
+ * Get a user-friendly display name based on the icon/provider.
260
+ */
261
+ getDefaultDisplayName(icon) {
262
+ switch (icon) {
263
+ case "aws-s3": return "AWS S3";
264
+ case "r2":
265
+ case "cloudflare":
266
+ case "cloudflare-r2": return "Cloudflare R2";
267
+ case "gcs":
268
+ case "google-cloud":
269
+ case "google-cloud-storage": return "Google Cloud Storage";
270
+ case "azure":
271
+ case "azure-blob": return "Azure Blob";
272
+ case "minio": return "MinIO";
273
+ case "s3": return "S3";
274
+ default: return;
275
+ }
276
+ }
277
+ getClient() {
278
+ if (this._client) return this._client;
279
+ const hasStaticCredentials = this.accessKeyId && this.secretAccessKey;
280
+ let credentials;
281
+ if (this.credentials) credentials = this.credentials;
282
+ else if (hasStaticCredentials) credentials = {
283
+ accessKeyId: this.accessKeyId,
284
+ secretAccessKey: this.secretAccessKey,
285
+ ...this.sessionToken && { sessionToken: this.sessionToken }
286
+ };
287
+ this._client = new S3Client({
288
+ region: this.region,
289
+ ...credentials !== void 0 && { credentials },
290
+ endpoint: this.endpoint,
291
+ forcePathStyle: this.forcePathStyle
292
+ });
293
+ return this._client;
294
+ }
295
+ /**
296
+ * Ensure the filesystem is initialized and return the S3 client.
297
+ * Uses base class ensureReady() for status management, then returns client.
298
+ */
299
+ async getReadyClient() {
300
+ await this.ensureReady();
301
+ return this.getClient();
302
+ }
303
+ toKey(path) {
304
+ const cleanPath = path.replace(/^\/+/, "").replace(/^\.(?:\/|$)/, "");
305
+ return this.prefix + cleanPath;
306
+ }
307
+ async readFile(path, options) {
308
+ const client = await this.getReadyClient();
309
+ try {
310
+ const body = await (await client.send(new GetObjectCommand({
311
+ Bucket: this.bucket,
312
+ Key: this.toKey(path)
313
+ }))).Body?.transformToByteArray();
314
+ if (!body) throw new FileNotFoundError(path);
315
+ const buffer = Buffer.from(body);
316
+ if (options?.encoding) return buffer.toString(options.encoding);
317
+ return buffer;
318
+ } catch (error) {
319
+ if (isNotFoundError$1(error)) throw new FileNotFoundError(path);
320
+ throw this.handleError(error);
321
+ }
322
+ }
323
+ async writeFile(path, content, options) {
324
+ const client = await this.getReadyClient();
325
+ if (options?.overwrite === false && await this.exists(path)) throw new FileExistsError(path);
326
+ const body = typeof content === "string" ? Buffer.from(content, "utf-8") : Buffer.from(content);
327
+ const contentType = getMimeType(path);
328
+ await client.send(new PutObjectCommand({
329
+ Bucket: this.bucket,
330
+ Key: this.toKey(path),
331
+ Body: body,
332
+ ContentType: contentType
333
+ }));
334
+ }
335
+ async appendFile(path, content) {
336
+ let existing = "";
337
+ try {
338
+ existing = await this.readFile(path, { encoding: "utf-8" });
339
+ } catch (error) {
340
+ if (error instanceof FileNotFoundError) {} else throw error;
341
+ }
342
+ const appendContent = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
343
+ await this.writeFile(path, existing + appendContent);
344
+ }
345
+ async deleteFile(path, options) {
346
+ if (await this.isDirectory(path)) {
347
+ await this.rmdir(path, {
348
+ recursive: true,
349
+ force: options?.force
350
+ });
351
+ return;
352
+ }
353
+ const client = await this.getReadyClient();
354
+ try {
355
+ await client.send(new DeleteObjectCommand({
356
+ Bucket: this.bucket,
357
+ Key: this.toKey(path)
358
+ }));
359
+ } catch (error) {
360
+ if (options?.force) return;
361
+ if (isNotFoundError$1(error)) throw new FileNotFoundError(path);
362
+ throw this.handleError(error);
363
+ }
364
+ }
365
+ async copyFile(src, dest, options) {
366
+ const client = await this.getReadyClient();
367
+ if (options?.overwrite === false && await this.exists(dest)) throw new FileExistsError(dest);
368
+ try {
369
+ await client.send(new CopyObjectCommand({
370
+ Bucket: this.bucket,
371
+ CopySource: `${this.bucket}/${encodeURIComponent(this.toKey(src)).replace(/%2F/g, "/")}`,
372
+ Key: this.toKey(dest)
373
+ }));
374
+ } catch (error) {
375
+ if (isNotFoundError$1(error)) throw new FileNotFoundError(src);
376
+ throw this.handleError(error);
377
+ }
378
+ }
379
+ async moveFile(src, dest, options) {
380
+ await this.copyFile(src, dest, options);
381
+ await this.deleteFile(src, { force: true });
382
+ }
383
+ async mkdir(_path, _options) {}
384
+ async rmdir(path, options) {
385
+ if (!options?.recursive) {
386
+ if ((await this.readdir(path)).length > 0) throw new Error(`Directory not empty: ${path}`);
387
+ return;
388
+ }
389
+ const client = await this.getReadyClient();
390
+ const prefix = this.toKey(path).replace(/\/$/, "") + "/";
391
+ let continuationToken;
392
+ do {
393
+ const listResponse = await client.send(new ListObjectsV2Command({
394
+ Bucket: this.bucket,
395
+ Prefix: prefix,
396
+ ContinuationToken: continuationToken
397
+ }));
398
+ if (listResponse.Contents && listResponse.Contents.length > 0) {
399
+ const deleteResponse = await client.send(new DeleteObjectsCommand({
400
+ Bucket: this.bucket,
401
+ Delete: { Objects: listResponse.Contents.filter((obj) => !!obj.Key).map((obj) => ({ Key: obj.Key })) }
402
+ }));
403
+ if (deleteResponse.Errors && deleteResponse.Errors.length > 0) throw new Error(`Failed to delete ${deleteResponse.Errors.length} object(s) in ${path}`);
404
+ }
405
+ continuationToken = listResponse.NextContinuationToken;
406
+ } while (continuationToken);
407
+ }
408
+ async readdir(path, options) {
409
+ const client = await this.getReadyClient();
410
+ const prefix = this.toKey(path).replace(/\/$/, "");
411
+ const searchPrefix = prefix ? prefix + "/" : "";
412
+ const entries = [];
413
+ const seenDirs = /* @__PURE__ */ new Set();
414
+ let continuationToken;
415
+ do {
416
+ const response = await client.send(new ListObjectsV2Command({
417
+ Bucket: this.bucket,
418
+ Prefix: searchPrefix,
419
+ Delimiter: options?.recursive ? void 0 : "/",
420
+ ContinuationToken: continuationToken
421
+ }));
422
+ if (response.Contents) for (const obj of response.Contents) {
423
+ const key = obj.Key;
424
+ if (!key || key === searchPrefix) continue;
425
+ const relativePath = key.slice(searchPrefix.length);
426
+ if (!relativePath) continue;
427
+ if (relativePath.endsWith("/")) {
428
+ const dirName = relativePath.slice(0, -1);
429
+ if (!seenDirs.has(dirName)) {
430
+ seenDirs.add(dirName);
431
+ entries.push({
432
+ name: dirName,
433
+ type: "directory"
434
+ });
435
+ }
436
+ continue;
437
+ }
438
+ const name = options?.recursive ? relativePath : relativePath.split("/")[0];
439
+ if (!name) continue;
440
+ if (options?.extension) {
441
+ if (!(Array.isArray(options.extension) ? options.extension : [options.extension]).some((ext) => name.endsWith(ext))) continue;
442
+ }
443
+ entries.push({
444
+ name,
445
+ type: "file",
446
+ size: obj.Size
447
+ });
448
+ }
449
+ if (response.CommonPrefixes) for (const prefixObj of response.CommonPrefixes) {
450
+ if (!prefixObj.Prefix) continue;
451
+ const dirName = prefixObj.Prefix.slice(searchPrefix.length).replace(/\/$/, "");
452
+ if (dirName && !seenDirs.has(dirName)) {
453
+ seenDirs.add(dirName);
454
+ entries.push({
455
+ name: dirName,
456
+ type: "directory"
457
+ });
458
+ }
459
+ }
460
+ continuationToken = response.NextContinuationToken;
461
+ } while (continuationToken);
462
+ return entries;
463
+ }
464
+ async exists(path) {
465
+ const key = this.toKey(path);
466
+ if (!key) return true;
467
+ const client = await this.getReadyClient();
468
+ try {
469
+ await client.send(new HeadObjectCommand({
470
+ Bucket: this.bucket,
471
+ Key: key
472
+ }));
473
+ return true;
474
+ } catch (error) {
475
+ if (!isNotFoundError$1(error)) throw this.handleError(error);
476
+ }
477
+ return ((await client.send(new ListObjectsV2Command({
478
+ Bucket: this.bucket,
479
+ Prefix: key.replace(/\/$/, "") + "/",
480
+ MaxKeys: 1
481
+ }))).Contents?.length ?? 0) > 0;
482
+ }
483
+ async stat(path) {
484
+ const key = this.toKey(path);
485
+ if (!key) return {
486
+ name: "",
487
+ path,
488
+ type: "directory",
489
+ size: 0,
490
+ createdAt: /* @__PURE__ */ new Date(),
491
+ modifiedAt: /* @__PURE__ */ new Date()
492
+ };
493
+ const client = await this.getReadyClient();
494
+ try {
495
+ const response = await client.send(new HeadObjectCommand({
496
+ Bucket: this.bucket,
497
+ Key: key
498
+ }));
499
+ return {
500
+ name: path.split("/").pop() ?? "",
501
+ path,
502
+ type: "file",
503
+ size: response.ContentLength ?? 0,
504
+ createdAt: response.LastModified ?? /* @__PURE__ */ new Date(),
505
+ modifiedAt: response.LastModified ?? /* @__PURE__ */ new Date()
506
+ };
507
+ } catch (error) {
508
+ if (!isNotFoundError$1(error)) throw this.handleError(error);
509
+ if (await this.isDirectory(path)) return {
510
+ name: path.split("/").filter(Boolean).pop() ?? "",
511
+ path,
512
+ type: "directory",
513
+ size: 0,
514
+ createdAt: /* @__PURE__ */ new Date(),
515
+ modifiedAt: /* @__PURE__ */ new Date()
516
+ };
517
+ throw new FileNotFoundError(path);
518
+ }
519
+ }
520
+ async isFile(path) {
521
+ const key = this.toKey(path);
522
+ if (!key) return false;
523
+ const client = await this.getReadyClient();
524
+ try {
525
+ await client.send(new HeadObjectCommand({
526
+ Bucket: this.bucket,
527
+ Key: key
528
+ }));
529
+ return true;
530
+ } catch (error) {
531
+ if (!isNotFoundError$1(error)) throw this.handleError(error);
532
+ return false;
533
+ }
534
+ }
535
+ async isDirectory(path) {
536
+ const key = this.toKey(path);
537
+ if (!key) return true;
538
+ return ((await (await this.getReadyClient()).send(new ListObjectsV2Command({
539
+ Bucket: this.bucket,
540
+ Prefix: key.replace(/\/$/, "") + "/",
541
+ MaxKeys: 1
542
+ }))).Contents?.length ?? 0) > 0;
543
+ }
544
+ /**
545
+ * Initialize the S3 client.
546
+ * Status management is handled by the base class.
547
+ */
548
+ async init() {
549
+ const client = this.getClient();
550
+ try {
551
+ await client.send(new HeadBucketCommand({ Bucket: this.bucket }));
552
+ } catch (error) {
553
+ const statusCode = error.$metadata?.httpStatusCode;
554
+ const createError = (message) => {
555
+ const err = new Error(message);
556
+ if (statusCode) err.status = statusCode;
557
+ return err;
558
+ };
559
+ if (isAccessDeniedError(error)) throw createError(`Access denied to bucket "${this.bucket}" - check credentials and permissions`);
560
+ if (isNotFoundError$1(error)) throw createError(`Bucket "${this.bucket}" not found`);
561
+ const message = error instanceof Error ? error.message : String(error);
562
+ if (statusCode) throw createError(`Failed to access bucket "${this.bucket}" (HTTP ${statusCode}): ${message}`);
563
+ throw error;
564
+ }
565
+ }
566
+ /**
567
+ * Clean up the S3 client.
568
+ * Status management is handled by the base class.
569
+ */
570
+ async destroy() {
571
+ this._client = null;
572
+ }
626
573
  };
627
- function trimSlashes2(s) {
628
- let start = 0;
629
- let end = s.length;
630
- while (start < end && s[start] === "/") start++;
631
- while (end > start && s[end - 1] === "/") end--;
632
- return s.slice(start, end);
574
+ //#endregion
575
+ //#region src/blob-store/index.ts
576
+ /** Trim leading and trailing slashes. */
577
+ function trimSlashes(s) {
578
+ let start = 0;
579
+ let end = s.length;
580
+ while (start < end && s[start] === "/") start++;
581
+ while (end > start && s[end - 1] === "/") end--;
582
+ return s.slice(start, end);
633
583
  }
584
+ /**
585
+ * S3-backed content-addressable blob store for skill versioning.
586
+ *
587
+ * Each blob is stored as an S3 object keyed by its SHA-256 hash.
588
+ * Metadata (size, mimeType, createdAt) is stored in S3 object user metadata.
589
+ *
590
+ * Since blobs are content-addressable, writes are idempotent — the same hash
591
+ * always maps to the same content, so overwrites are safe and equivalent to
592
+ * a no-op.
593
+ *
594
+ * @example AWS S3
595
+ * ```typescript
596
+ * import { S3BlobStore } from '@mastra/s3';
597
+ *
598
+ * const blobs = new S3BlobStore({
599
+ * bucket: 'my-skill-blobs',
600
+ * region: 'us-east-1',
601
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
602
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
603
+ * });
604
+ * ```
605
+ *
606
+ * @example MinIO (local)
607
+ * ```typescript
608
+ * import { S3BlobStore } from '@mastra/s3';
609
+ *
610
+ * const blobs = new S3BlobStore({
611
+ * bucket: 'skill-blobs',
612
+ * region: 'us-east-1',
613
+ * accessKeyId: 'minioadmin',
614
+ * secretAccessKey: 'minioadmin',
615
+ * endpoint: 'http://localhost:9000',
616
+ * forcePathStyle: true,
617
+ * });
618
+ * ```
619
+ */
634
620
  var S3BlobStore = class extends BlobStore {
635
- bucket;
636
- prefix;
637
- _client = null;
638
- region;
639
- credentials;
640
- accessKeyId;
641
- secretAccessKey;
642
- sessionToken;
643
- endpoint;
644
- forcePathStyle;
645
- constructor(options) {
646
- super();
647
- this.bucket = options.bucket;
648
- this.region = options.region;
649
- this.credentials = options.credentials;
650
- this.accessKeyId = options.accessKeyId;
651
- this.secretAccessKey = options.secretAccessKey;
652
- this.sessionToken = options.sessionToken;
653
- this.endpoint = options.endpoint;
654
- this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
655
- this.prefix = options.prefix ? trimSlashes2(options.prefix) + "/" : "mastra_skill_blobs/";
656
- }
657
- getClient() {
658
- if (this._client) return this._client;
659
- const hasStaticCredentials = this.accessKeyId && this.secretAccessKey;
660
- let credentials;
661
- if (this.credentials) {
662
- credentials = this.credentials;
663
- } else if (hasStaticCredentials) {
664
- credentials = {
665
- accessKeyId: this.accessKeyId,
666
- secretAccessKey: this.secretAccessKey,
667
- ...this.sessionToken && { sessionToken: this.sessionToken }
668
- };
669
- }
670
- this._client = new S3Client({
671
- region: this.region,
672
- ...credentials !== void 0 && { credentials },
673
- endpoint: this.endpoint,
674
- forcePathStyle: this.forcePathStyle
675
- });
676
- return this._client;
677
- }
678
- toKey(hash) {
679
- return this.prefix + hash;
680
- }
681
- async init() {
682
- }
683
- async put(entry) {
684
- const client = this.getClient();
685
- const now = entry.createdAt ?? /* @__PURE__ */ new Date();
686
- await client.send(
687
- new PutObjectCommand({
688
- Bucket: this.bucket,
689
- Key: this.toKey(entry.hash),
690
- Body: entry.content,
691
- ContentType: entry.mimeType ?? "application/octet-stream",
692
- Metadata: {
693
- size: String(entry.size),
694
- createdat: now.toISOString(),
695
- ...entry.mimeType ? { mimetype: entry.mimeType } : {}
696
- }
697
- })
698
- );
699
- }
700
- async get(hash) {
701
- const client = this.getClient();
702
- try {
703
- const response = await client.send(
704
- new GetObjectCommand({
705
- Bucket: this.bucket,
706
- Key: this.toKey(hash)
707
- })
708
- );
709
- const body = await response.Body?.transformToString("utf-8");
710
- if (body === void 0 || body === null) return null;
711
- const metadata = response.Metadata ?? {};
712
- return {
713
- hash,
714
- content: body,
715
- size: metadata.size != null ? Number(metadata.size) : Buffer.byteLength(body, "utf-8"),
716
- mimeType: metadata.mimetype || response.ContentType || void 0,
717
- createdAt: metadata.createdat ? new Date(metadata.createdat) : /* @__PURE__ */ new Date()
718
- };
719
- } catch (error) {
720
- if (isNotFoundError2(error)) return null;
721
- throw error;
722
- }
723
- }
724
- async has(hash) {
725
- const client = this.getClient();
726
- try {
727
- await client.send(
728
- new HeadObjectCommand({
729
- Bucket: this.bucket,
730
- Key: this.toKey(hash)
731
- })
732
- );
733
- return true;
734
- } catch (error) {
735
- if (isNotFoundError2(error)) return false;
736
- throw error;
737
- }
738
- }
739
- async delete(hash) {
740
- const existed = await this.has(hash);
741
- if (!existed) return false;
742
- const client = this.getClient();
743
- await client.send(
744
- new DeleteObjectCommand({
745
- Bucket: this.bucket,
746
- Key: this.toKey(hash)
747
- })
748
- );
749
- return true;
750
- }
751
- async putMany(entries) {
752
- if (entries.length === 0) return;
753
- await Promise.all(entries.map((entry) => this.put(entry)));
754
- }
755
- async getMany(hashes) {
756
- const result = /* @__PURE__ */ new Map();
757
- if (hashes.length === 0) return result;
758
- const entries = await Promise.all(hashes.map((hash) => this.get(hash)));
759
- for (const entry of entries) {
760
- if (entry) {
761
- result.set(entry.hash, entry);
762
- }
763
- }
764
- return result;
765
- }
766
- async dangerouslyClearAll() {
767
- const client = this.getClient();
768
- let continuationToken;
769
- do {
770
- const listResponse = await client.send(
771
- new ListObjectsV2Command({
772
- Bucket: this.bucket,
773
- Prefix: this.prefix,
774
- ContinuationToken: continuationToken
775
- })
776
- );
777
- const objects = listResponse.Contents;
778
- if (objects && objects.length > 0) {
779
- await client.send(
780
- new DeleteObjectsCommand({
781
- Bucket: this.bucket,
782
- Delete: {
783
- Objects: objects.filter((obj) => obj.Key != null).map((obj) => ({ Key: obj.Key })),
784
- Quiet: true
785
- }
786
- })
787
- );
788
- }
789
- continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : void 0;
790
- } while (continuationToken);
791
- }
621
+ bucket;
622
+ prefix;
623
+ _client = null;
624
+ region;
625
+ credentials;
626
+ accessKeyId;
627
+ secretAccessKey;
628
+ sessionToken;
629
+ endpoint;
630
+ forcePathStyle;
631
+ constructor(options) {
632
+ super();
633
+ this.bucket = options.bucket;
634
+ this.region = options.region;
635
+ this.credentials = options.credentials;
636
+ this.accessKeyId = options.accessKeyId;
637
+ this.secretAccessKey = options.secretAccessKey;
638
+ this.sessionToken = options.sessionToken;
639
+ this.endpoint = options.endpoint;
640
+ this.forcePathStyle = options.forcePathStyle ?? !!options.endpoint;
641
+ this.prefix = options.prefix ? trimSlashes(options.prefix) + "/" : "mastra_skill_blobs/";
642
+ }
643
+ getClient() {
644
+ if (this._client) return this._client;
645
+ const hasStaticCredentials = this.accessKeyId && this.secretAccessKey;
646
+ let credentials;
647
+ if (this.credentials) credentials = this.credentials;
648
+ else if (hasStaticCredentials) credentials = {
649
+ accessKeyId: this.accessKeyId,
650
+ secretAccessKey: this.secretAccessKey,
651
+ ...this.sessionToken && { sessionToken: this.sessionToken }
652
+ };
653
+ this._client = new S3Client({
654
+ region: this.region,
655
+ ...credentials !== void 0 && { credentials },
656
+ endpoint: this.endpoint,
657
+ forcePathStyle: this.forcePathStyle
658
+ });
659
+ return this._client;
660
+ }
661
+ toKey(hash) {
662
+ return this.prefix + hash;
663
+ }
664
+ async init() {}
665
+ async put(entry) {
666
+ const client = this.getClient();
667
+ const now = entry.createdAt ?? /* @__PURE__ */ new Date();
668
+ await client.send(new PutObjectCommand({
669
+ Bucket: this.bucket,
670
+ Key: this.toKey(entry.hash),
671
+ Body: entry.content,
672
+ ContentType: entry.mimeType ?? "application/octet-stream",
673
+ Metadata: {
674
+ size: String(entry.size),
675
+ createdat: now.toISOString(),
676
+ ...entry.mimeType ? { mimetype: entry.mimeType } : {}
677
+ }
678
+ }));
679
+ }
680
+ async get(hash) {
681
+ const client = this.getClient();
682
+ try {
683
+ const response = await client.send(new GetObjectCommand({
684
+ Bucket: this.bucket,
685
+ Key: this.toKey(hash)
686
+ }));
687
+ const body = await response.Body?.transformToString("utf-8");
688
+ if (body === void 0 || body === null) return null;
689
+ const metadata = response.Metadata ?? {};
690
+ return {
691
+ hash,
692
+ content: body,
693
+ size: metadata.size != null ? Number(metadata.size) : Buffer.byteLength(body, "utf-8"),
694
+ mimeType: metadata.mimetype || response.ContentType || void 0,
695
+ createdAt: metadata.createdat ? new Date(metadata.createdat) : /* @__PURE__ */ new Date()
696
+ };
697
+ } catch (error) {
698
+ if (isNotFoundError(error)) return null;
699
+ throw error;
700
+ }
701
+ }
702
+ async has(hash) {
703
+ const client = this.getClient();
704
+ try {
705
+ await client.send(new HeadObjectCommand({
706
+ Bucket: this.bucket,
707
+ Key: this.toKey(hash)
708
+ }));
709
+ return true;
710
+ } catch (error) {
711
+ if (isNotFoundError(error)) return false;
712
+ throw error;
713
+ }
714
+ }
715
+ async delete(hash) {
716
+ if (!await this.has(hash)) return false;
717
+ await this.getClient().send(new DeleteObjectCommand({
718
+ Bucket: this.bucket,
719
+ Key: this.toKey(hash)
720
+ }));
721
+ return true;
722
+ }
723
+ async putMany(entries) {
724
+ if (entries.length === 0) return;
725
+ await Promise.all(entries.map((entry) => this.put(entry)));
726
+ }
727
+ async getMany(hashes) {
728
+ const result = /* @__PURE__ */ new Map();
729
+ if (hashes.length === 0) return result;
730
+ const entries = await Promise.all(hashes.map((hash) => this.get(hash)));
731
+ for (const entry of entries) if (entry) result.set(entry.hash, entry);
732
+ return result;
733
+ }
734
+ async dangerouslyClearAll() {
735
+ const client = this.getClient();
736
+ let continuationToken;
737
+ do {
738
+ const listResponse = await client.send(new ListObjectsV2Command({
739
+ Bucket: this.bucket,
740
+ Prefix: this.prefix,
741
+ ContinuationToken: continuationToken
742
+ }));
743
+ const objects = listResponse.Contents;
744
+ if (objects && objects.length > 0) await client.send(new DeleteObjectsCommand({
745
+ Bucket: this.bucket,
746
+ Delete: {
747
+ Objects: objects.filter((obj) => obj.Key != null).map((obj) => ({ Key: obj.Key })),
748
+ Quiet: true
749
+ }
750
+ }));
751
+ continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : void 0;
752
+ } while (continuationToken);
753
+ }
792
754
  };
793
- function isNotFoundError2(error) {
794
- if (!error || typeof error !== "object" || !("name" in error)) return false;
795
- const name = error.name;
796
- return name === "NotFound" || name === "NoSuchKey" || name === "404";
755
+ function isNotFoundError(error) {
756
+ if (!error || typeof error !== "object" || !("name" in error)) return false;
757
+ const name = error.name;
758
+ return name === "NotFound" || name === "NoSuchKey" || name === "404";
797
759
  }
798
-
799
- // src/provider.ts
800
- var s3FilesystemProvider = {
801
- id: "s3",
802
- name: "Amazon S3",
803
- description: "S3 or S3-compatible storage (AWS, R2, MinIO, DO Spaces)",
804
- configSchema: {
805
- type: "object",
806
- required: ["bucket", "region"],
807
- properties: {
808
- bucket: { type: "string", description: "S3 bucket name" },
809
- region: { type: "string", description: 'AWS region (use "auto" for R2)' },
810
- accessKeyId: { type: "string", description: "AWS access key ID" },
811
- secretAccessKey: { type: "string", description: "AWS secret access key" },
812
- sessionToken: { type: "string", description: "AWS session token for temporary credentials" },
813
- endpoint: { type: "string", description: "Custom endpoint URL for S3-compatible storage" },
814
- forcePathStyle: { type: "boolean", description: "Force path-style URLs", default: false },
815
- prefix: { type: "string", description: "Key prefix (acts like a subdirectory)" },
816
- readOnly: { type: "boolean", description: "Mount as read-only", default: false }
817
- }
818
- },
819
- createFilesystem: (config) => new S3Filesystem(config)
760
+ //#endregion
761
+ //#region src/provider.ts
762
+ const s3FilesystemProvider = {
763
+ id: "s3",
764
+ name: "Amazon S3",
765
+ description: "S3 or S3-compatible storage (AWS, R2, MinIO, DO Spaces)",
766
+ configSchema: {
767
+ type: "object",
768
+ required: ["bucket", "region"],
769
+ properties: {
770
+ bucket: {
771
+ type: "string",
772
+ description: "S3 bucket name"
773
+ },
774
+ region: {
775
+ type: "string",
776
+ description: "AWS region (use \"auto\" for R2)"
777
+ },
778
+ accessKeyId: {
779
+ type: "string",
780
+ description: "AWS access key ID"
781
+ },
782
+ secretAccessKey: {
783
+ type: "string",
784
+ description: "AWS secret access key"
785
+ },
786
+ sessionToken: {
787
+ type: "string",
788
+ description: "AWS session token for temporary credentials"
789
+ },
790
+ endpoint: {
791
+ type: "string",
792
+ description: "Custom endpoint URL for S3-compatible storage"
793
+ },
794
+ forcePathStyle: {
795
+ type: "boolean",
796
+ description: "Force path-style URLs",
797
+ default: false
798
+ },
799
+ prefix: {
800
+ type: "string",
801
+ description: "Key prefix (acts like a subdirectory)"
802
+ },
803
+ readOnly: {
804
+ type: "boolean",
805
+ description: "Mount as read-only",
806
+ default: false
807
+ }
808
+ }
809
+ },
810
+ createFilesystem: (config) => new S3Filesystem(config)
820
811
  };
821
- var s3BlobStoreProvider = {
822
- id: "s3",
823
- name: "Amazon S3 Blob Store",
824
- description: "Content-addressable blob storage using S3 or S3-compatible storage (AWS, R2, MinIO, DO Spaces)",
825
- configSchema: {
826
- type: "object",
827
- required: ["bucket", "region", "accessKeyId", "secretAccessKey"],
828
- properties: {
829
- bucket: { type: "string", description: "S3 bucket name" },
830
- region: { type: "string", description: 'AWS region (use "auto" for R2)' },
831
- accessKeyId: { type: "string", description: "AWS access key ID" },
832
- secretAccessKey: { type: "string", description: "AWS secret access key" },
833
- sessionToken: { type: "string", description: "AWS session token for temporary credentials" },
834
- endpoint: { type: "string", description: "Custom endpoint URL for S3-compatible storage" },
835
- forcePathStyle: { type: "boolean", description: "Force path-style URLs", default: false },
836
- prefix: { type: "string", description: "Key prefix for blob objects (default: mastra_skill_blobs/)" }
837
- }
838
- },
839
- createBlobStore: (config) => new S3BlobStore(config)
812
+ /**
813
+ * S3 blob store provider descriptor for MastraEditor.
814
+ *
815
+ * @example
816
+ * ```typescript
817
+ * import { s3BlobStoreProvider } from '@mastra/s3';
818
+ *
819
+ * const editor = new MastraEditor({
820
+ * blobStores: { s3: s3BlobStoreProvider },
821
+ * });
822
+ * ```
823
+ */
824
+ const s3BlobStoreProvider = {
825
+ id: "s3",
826
+ name: "Amazon S3 Blob Store",
827
+ description: "Content-addressable blob storage using S3 or S3-compatible storage (AWS, R2, MinIO, DO Spaces)",
828
+ configSchema: {
829
+ type: "object",
830
+ required: [
831
+ "bucket",
832
+ "region",
833
+ "accessKeyId",
834
+ "secretAccessKey"
835
+ ],
836
+ properties: {
837
+ bucket: {
838
+ type: "string",
839
+ description: "S3 bucket name"
840
+ },
841
+ region: {
842
+ type: "string",
843
+ description: "AWS region (use \"auto\" for R2)"
844
+ },
845
+ accessKeyId: {
846
+ type: "string",
847
+ description: "AWS access key ID"
848
+ },
849
+ secretAccessKey: {
850
+ type: "string",
851
+ description: "AWS secret access key"
852
+ },
853
+ sessionToken: {
854
+ type: "string",
855
+ description: "AWS session token for temporary credentials"
856
+ },
857
+ endpoint: {
858
+ type: "string",
859
+ description: "Custom endpoint URL for S3-compatible storage"
860
+ },
861
+ forcePathStyle: {
862
+ type: "boolean",
863
+ description: "Force path-style URLs",
864
+ default: false
865
+ },
866
+ prefix: {
867
+ type: "string",
868
+ description: "Key prefix for blob objects (default: mastra_skill_blobs/)"
869
+ }
870
+ }
871
+ },
872
+ createBlobStore: (config) => new S3BlobStore(config)
840
873
  };
841
-
874
+ //#endregion
842
875
  export { S3BlobStore, S3Filesystem, s3BlobStoreProvider, s3FilesystemProvider };
843
- //# sourceMappingURL=index.js.map
876
+
844
877
  //# sourceMappingURL=index.js.map