@procedures/s3 0.0.0-stage → 0.2.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +886 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/index.d.ts +18 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +20 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/module.d.ts +22 -0
  10. package/dist/module.d.ts.map +1 -0
  11. package/dist/module.js +173 -0
  12. package/dist/module.js.map +1 -0
  13. package/dist/procedures/s3/delete.d.ts +26 -0
  14. package/dist/procedures/s3/delete.d.ts.map +1 -0
  15. package/dist/procedures/s3/delete.js +40 -0
  16. package/dist/procedures/s3/delete.js.map +1 -0
  17. package/dist/procedures/s3/download.d.ts +36 -0
  18. package/dist/procedures/s3/download.d.ts.map +1 -0
  19. package/dist/procedures/s3/download.js +143 -0
  20. package/dist/procedures/s3/download.js.map +1 -0
  21. package/dist/procedures/s3/index.d.ts +13 -0
  22. package/dist/procedures/s3/index.d.ts.map +1 -0
  23. package/dist/procedures/s3/index.js +13 -0
  24. package/dist/procedures/s3/index.js.map +1 -0
  25. package/dist/procedures/s3/list-all.d.ts +24 -0
  26. package/dist/procedures/s3/list-all.d.ts.map +1 -0
  27. package/dist/procedures/s3/list-all.js +57 -0
  28. package/dist/procedures/s3/list-all.js.map +1 -0
  29. package/dist/procedures/s3/list.d.ts +28 -0
  30. package/dist/procedures/s3/list.d.ts.map +1 -0
  31. package/dist/procedures/s3/list.js +56 -0
  32. package/dist/procedures/s3/list.js.map +1 -0
  33. package/dist/procedures/s3/multipart-abort.d.ts +21 -0
  34. package/dist/procedures/s3/multipart-abort.d.ts.map +1 -0
  35. package/dist/procedures/s3/multipart-abort.js +33 -0
  36. package/dist/procedures/s3/multipart-abort.js.map +1 -0
  37. package/dist/procedures/s3/multipart-complete.d.ts +26 -0
  38. package/dist/procedures/s3/multipart-complete.d.ts.map +1 -0
  39. package/dist/procedures/s3/multipart-complete.js +51 -0
  40. package/dist/procedures/s3/multipart-complete.js.map +1 -0
  41. package/dist/procedures/s3/multipart-init.d.ts +24 -0
  42. package/dist/procedures/s3/multipart-init.d.ts.map +1 -0
  43. package/dist/procedures/s3/multipart-init.js +42 -0
  44. package/dist/procedures/s3/multipart-init.js.map +1 -0
  45. package/dist/procedures/s3/multipart-upload.d.ts +24 -0
  46. package/dist/procedures/s3/multipart-upload.d.ts.map +1 -0
  47. package/dist/procedures/s3/multipart-upload.js +44 -0
  48. package/dist/procedures/s3/multipart-upload.js.map +1 -0
  49. package/dist/procedures/s3/upload.d.ts +30 -0
  50. package/dist/procedures/s3/upload.d.ts.map +1 -0
  51. package/dist/procedures/s3/upload.js +50 -0
  52. package/dist/procedures/s3/upload.js.map +1 -0
  53. package/dist/register.d.ts +7 -0
  54. package/dist/register.d.ts.map +1 -0
  55. package/dist/register.js +9 -0
  56. package/dist/register.js.map +1 -0
  57. package/dist/s3-client.d.ts +38 -0
  58. package/dist/s3-client.d.ts.map +1 -0
  59. package/dist/s3-client.js +83 -0
  60. package/dist/s3-client.js.map +1 -0
  61. package/dist/types.d.ts +198 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +197 -0
  64. package/dist/types.js.map +1 -0
  65. package/package.json +80 -3
  66. package/src/index.ts +72 -0
  67. package/src/module.ts +234 -0
  68. package/src/procedures/s3/delete.ts +45 -0
  69. package/src/procedures/s3/download.ts +155 -0
  70. package/src/procedures/s3/index.ts +13 -0
  71. package/src/procedures/s3/list-all.ts +63 -0
  72. package/src/procedures/s3/list.ts +63 -0
  73. package/src/procedures/s3/multipart-abort.ts +40 -0
  74. package/src/procedures/s3/multipart-complete.ts +58 -0
  75. package/src/procedures/s3/multipart-init.ts +50 -0
  76. package/src/procedures/s3/multipart-upload.ts +53 -0
  77. package/src/procedures/s3/upload.ts +56 -0
  78. package/src/register.ts +11 -0
  79. package/src/s3-client.ts +94 -0
  80. package/src/types.ts +425 -0
package/src/module.ts ADDED
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Module (no side effects): the procedures of this package as data, for `createRegistry()`.
3
+ *
4
+ * Procedure Registration for S3 operations
5
+ *
6
+ * Provides s3.upload, s3.download, s3.list, s3.delete, and s3.multipart.* procedures.
7
+ * Credentials are loaded from environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY).
8
+ */
9
+
10
+ import { createProcedure, zodAdapter, defineModule, type ProcedureModule, PROCEDURE_REGISTRY, registerModules, type ProcedureRegistry } from "@procedures/client";
11
+ import {
12
+ s3Upload,
13
+ s3Download,
14
+ s3List,
15
+ s3ListAll,
16
+ s3Delete,
17
+ s3MultipartInit,
18
+ s3MultipartUpload,
19
+ s3MultipartComplete,
20
+ s3MultipartAbort,
21
+ } from "./procedures/s3/index.js";
22
+ import {
23
+ S3UploadInputSchema,
24
+ S3DownloadInputSchema,
25
+ S3ListInputSchema,
26
+ S3ListAllInputSchema,
27
+ S3DeleteInputSchema,
28
+ S3MultipartInitInputSchema,
29
+ S3MultipartUploadInputSchema,
30
+ S3MultipartCompleteInputSchema,
31
+ S3MultipartAbortInputSchema,
32
+ type S3UploadInput,
33
+ type S3DownloadInput,
34
+ type S3ListInput,
35
+ type S3ListAllInput,
36
+ type S3DeleteInput,
37
+ type S3MultipartInitInput,
38
+ type S3MultipartUploadInput,
39
+ type S3MultipartCompleteInput,
40
+ type S3MultipartAbortInput,
41
+ type S3UploadOutput,
42
+ type S3DownloadOutput,
43
+ type S3ListOutput,
44
+ type S3ListAllOutput,
45
+ type S3DeleteOutput,
46
+ type S3MultipartInitOutput,
47
+ type S3MultipartUploadOutput,
48
+ type S3MultipartCompleteOutput,
49
+ type S3MultipartAbortOutput,
50
+ S3UploadOutputSchema,
51
+ S3DownloadOutputSchema,
52
+ S3ListOutputSchema,
53
+ S3ListAllOutputSchema,
54
+ S3DeleteOutputSchema,
55
+ S3MultipartInitOutputSchema,
56
+ S3MultipartUploadOutputSchema,
57
+ S3MultipartCompleteOutputSchema,
58
+ S3MultipartAbortOutputSchema,
59
+ } from "./types.js";
60
+
61
+ // =============================================================================
62
+ // Procedures
63
+ // =============================================================================
64
+
65
+ const s3UploadProcedure = createProcedure()
66
+ .path(["s3", "upload"])
67
+ .input(zodAdapter<S3UploadInput>(S3UploadInputSchema))
68
+ .output(zodAdapter<S3UploadOutput>(S3UploadOutputSchema))
69
+ .meta({
70
+ description: "Upload content to S3 bucket",
71
+ // A put replaces the object of an existing key
72
+ effects: { reads: ["s3", "network"], writes: ["s3", "network"] },
73
+ shorts: { bucket: "b", key: "k" },
74
+ output: "json",
75
+ })
76
+ .handler(async (input: S3UploadInput): Promise<S3UploadOutput> => {
77
+ return s3Upload(input);
78
+ })
79
+ .build();
80
+
81
+ const s3DownloadProcedure = createProcedure()
82
+ .path(["s3", "download"])
83
+ .input(zodAdapter<S3DownloadInput>(S3DownloadInputSchema))
84
+ .output(zodAdapter<S3DownloadOutput>(S3DownloadOutputSchema))
85
+ .meta({
86
+ description: "Download content from S3 bucket",
87
+ // With destPath, it replaces the file at that path
88
+ effects: { reads: ["s3", "network"], writes: ["fs"], idempotent: true },
89
+ shorts: { bucket: "b", key: "k", encoding: "e" },
90
+ output: "json",
91
+ })
92
+ .handler(async (input: S3DownloadInput): Promise<S3DownloadOutput> => {
93
+ return s3Download(input);
94
+ })
95
+ .build();
96
+
97
+ const s3ListProcedure = createProcedure()
98
+ .path(["s3", "list"])
99
+ .input(zodAdapter<S3ListInput>(S3ListInputSchema))
100
+ .output(zodAdapter<S3ListOutput>(S3ListOutputSchema))
101
+ .meta({
102
+ description: "List objects in S3 bucket",
103
+ effects: { reads: ["s3", "network"], writes: [] },
104
+ shorts: { bucket: "b", prefix: "p", maxKeys: "n" },
105
+ output: "json",
106
+ })
107
+ .handler(async (input: S3ListInput): Promise<S3ListOutput> => {
108
+ return s3List(input);
109
+ })
110
+ .build();
111
+
112
+ const s3ListAllProcedure = createProcedure()
113
+ .path(["s3", "listAll"])
114
+ .input(zodAdapter<S3ListAllInput>(S3ListAllInputSchema))
115
+ .output(zodAdapter<S3ListAllOutput>(S3ListAllOutputSchema))
116
+ .meta({
117
+ description: "List ALL objects in S3 bucket, following continuation tokens (unbounded by 1000)",
118
+ effects: { reads: ["s3", "network"], writes: [] },
119
+ shorts: { bucket: "b", prefix: "p", maxKeys: "n" },
120
+ output: "json",
121
+ })
122
+ .handler(async (input: S3ListAllInput): Promise<S3ListAllOutput> => {
123
+ return s3ListAll(input);
124
+ })
125
+ .build();
126
+
127
+ const s3DeleteProcedure = createProcedure()
128
+ .path(["s3", "delete"])
129
+ .input(zodAdapter<S3DeleteInput>(S3DeleteInputSchema))
130
+ .output(zodAdapter<S3DeleteOutput>(S3DeleteOutputSchema))
131
+ .meta({
132
+ description: "Delete object from S3 bucket",
133
+ effects: { reads: ["s3", "network"], writes: ["s3", "network"] },
134
+ shorts: { bucket: "b", key: "k" },
135
+ output: "json",
136
+ })
137
+ .handler(async (input: S3DeleteInput): Promise<S3DeleteOutput> => {
138
+ return s3Delete(input);
139
+ })
140
+ .build();
141
+
142
+ const s3MultipartInitProcedure = createProcedure()
143
+ .path(["s3", "multipart", "init"])
144
+ .input(zodAdapter<S3MultipartInitInput>(S3MultipartInitInputSchema))
145
+ .output(zodAdapter<S3MultipartInitOutput>(S3MultipartInitOutputSchema))
146
+ .meta({
147
+ description: "Initialize multipart upload for large files",
148
+ // It starts a new upload: the object of the key does not change before the completion
149
+ effects: { reads: ["s3", "network"], writes: ["s3", "network"], destructive: false },
150
+ shorts: { bucket: "b", key: "k" },
151
+ output: "json",
152
+ })
153
+ .handler(async (input: S3MultipartInitInput): Promise<S3MultipartInitOutput> => {
154
+ return s3MultipartInit(input);
155
+ })
156
+ .build();
157
+
158
+ const s3MultipartUploadProcedure = createProcedure()
159
+ .path(["s3", "multipart", "upload"])
160
+ .input(zodAdapter<S3MultipartUploadInput>(S3MultipartUploadInputSchema))
161
+ .output(zodAdapter<S3MultipartUploadOutput>(S3MultipartUploadOutputSchema))
162
+ .meta({
163
+ description: "Upload a part in multipart upload",
164
+ // A part replaces the earlier part with the same number
165
+ effects: { reads: ["s3", "network"], writes: ["s3", "network"], idempotent: true },
166
+ shorts: { bucket: "b", key: "k", partNumber: "n" },
167
+ output: "json",
168
+ })
169
+ .handler(async (input: S3MultipartUploadInput): Promise<S3MultipartUploadOutput> => {
170
+ return s3MultipartUpload(input);
171
+ })
172
+ .build();
173
+
174
+ const s3MultipartCompleteProcedure = createProcedure()
175
+ .path(["s3", "multipart", "complete"])
176
+ .input(zodAdapter<S3MultipartCompleteInput>(S3MultipartCompleteInputSchema))
177
+ .output(zodAdapter<S3MultipartCompleteOutput>(S3MultipartCompleteOutputSchema))
178
+ .meta({
179
+ description: "Complete multipart upload by assembling parts",
180
+ // The assembled object replaces the object of an existing key
181
+ effects: { reads: ["s3", "network"], writes: ["s3", "network"] },
182
+ shorts: { bucket: "b", key: "k" },
183
+ output: "json",
184
+ })
185
+ .handler(async (input: S3MultipartCompleteInput): Promise<S3MultipartCompleteOutput> => {
186
+ return s3MultipartComplete(input);
187
+ })
188
+ .build();
189
+
190
+ const s3MultipartAbortProcedure = createProcedure()
191
+ .path(["s3", "multipart", "abort"])
192
+ .input(zodAdapter<S3MultipartAbortInput>(S3MultipartAbortInputSchema))
193
+ .output(zodAdapter<S3MultipartAbortOutput>(S3MultipartAbortOutputSchema))
194
+ .meta({
195
+ description: "Abort multipart upload and clean up parts",
196
+ effects: { reads: ["s3", "network"], writes: ["s3", "network"] },
197
+ shorts: { bucket: "b", key: "k" },
198
+ output: "json",
199
+ })
200
+ .handler(async (input: S3MultipartAbortInput): Promise<S3MultipartAbortOutput> => {
201
+ return s3MultipartAbort(input);
202
+ })
203
+ .build();
204
+
205
+ // =============================================================================
206
+ // Registration
207
+ // =============================================================================
208
+
209
+ /**
210
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
211
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
212
+ */
213
+ export const s3Module: ProcedureModule = defineModule("@procedures/s3", [
214
+ s3UploadProcedure,
215
+ s3DownloadProcedure,
216
+ s3ListProcedure,
217
+ s3ListAllProcedure,
218
+ s3DeleteProcedure,
219
+ s3MultipartInitProcedure,
220
+ s3MultipartUploadProcedure,
221
+ s3MultipartCompleteProcedure,
222
+ s3MultipartAbortProcedure,
223
+ ]);
224
+
225
+ export default s3Module;
226
+
227
+ /**
228
+ * This function registers the procedures of @procedures/s3, and of the modules that it requires, in
229
+ * a registry. The default is the global registry. Importing this file registers nothing: the
230
+ * `./register` entry starts this function (ARCHITECTURE-PROPOSALS-2026-10, P9).
231
+ */
232
+ export function registerS3Procedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
233
+ registerModules(registry, [s3Module]);
234
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * s3.delete procedure
3
+ *
4
+ * Delete object from S3 bucket.
5
+ */
6
+
7
+ import { DeleteObjectCommand } from "@aws-sdk/client-s3";
8
+ import { getS3Client } from "../../s3-client.js";
9
+ import type { S3DeleteInput, S3DeleteOutput } from "../../types.js";
10
+
11
+ /**
12
+ * Delete object from S3
13
+ *
14
+ * @example
15
+ * // Delete an object
16
+ * await client.call(["s3", "delete"], {
17
+ * bucket: "my-bucket",
18
+ * key: "path/to/file.json",
19
+ * });
20
+ *
21
+ * @example
22
+ * // Delete specific version
23
+ * await client.call(["s3", "delete"], {
24
+ * bucket: "my-bucket",
25
+ * key: "path/to/file.json",
26
+ * versionId: "abc123",
27
+ * });
28
+ */
29
+ export async function s3Delete(input: S3DeleteInput): Promise<S3DeleteOutput> {
30
+ const client = getS3Client();
31
+
32
+ const command = new DeleteObjectCommand({
33
+ Bucket: input.bucket,
34
+ Key: input.key,
35
+ VersionId: input.versionId,
36
+ });
37
+
38
+ const response = await client.send(command);
39
+
40
+ return {
41
+ deleted: true,
42
+ deleteMarker: response.DeleteMarker,
43
+ versionId: response.VersionId,
44
+ };
45
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * s3.download procedure
3
+ *
4
+ * Download content from S3 bucket.
5
+ */
6
+
7
+ import { randomUUID } from "node:crypto";
8
+ import { createWriteStream } from "node:fs";
9
+ import { rename, rm } from "node:fs/promises";
10
+ import { Transform, type Readable } from "node:stream";
11
+ import { pipeline } from "node:stream/promises";
12
+ import { GetObjectCommand } from "@aws-sdk/client-s3";
13
+ import { getS3Client } from "../../s3-client.js";
14
+ import type { S3DownloadInput, S3DownloadOutput } from "../../types.js";
15
+
16
+ /** Largest object loaded into memory by default (the base64 body is a third larger again) */
17
+ const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
18
+
19
+ function tooLarge(bytes: number | string, maxBytes: number): Error {
20
+ return new Error(
21
+ `Object is ${bytes} bytes, more than maxBytes (${maxBytes}). Use destPath to download it to a file.`
22
+ );
23
+ }
24
+
25
+ /** A stream step that counts the bytes and fails past `maxBytes`. */
26
+ function byteLimit(maxBytes: number): Transform {
27
+ let total = 0;
28
+ return new Transform({
29
+ transform(chunk: Buffer, _encoding, callback) {
30
+ total += chunk.length;
31
+ if (total > maxBytes) {
32
+ callback(tooLarge(`more than ${maxBytes}`, maxBytes));
33
+ return;
34
+ }
35
+ callback(null, chunk);
36
+ },
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Read a body into memory, counting the bytes. `Content-Length` can be missing or wrong, so the
42
+ * count is the limit (deep dive DATA-14).
43
+ */
44
+ async function readLimited(body: Readable, maxBytes: number): Promise<Buffer> {
45
+ const chunks: Buffer[] = [];
46
+ let total = 0;
47
+ for await (const chunk of body) {
48
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);
49
+ total += buffer.length;
50
+ if (total > maxBytes) {
51
+ body.destroy();
52
+ throw tooLarge(`more than ${maxBytes}`, maxBytes);
53
+ }
54
+ chunks.push(buffer);
55
+ }
56
+ return Buffer.concat(chunks);
57
+ }
58
+
59
+ /**
60
+ * Download content from S3
61
+ *
62
+ * @example
63
+ * // Download text file
64
+ * const result = await client.call(["s3", "download"], {
65
+ * bucket: "my-bucket",
66
+ * key: "path/to/file.json",
67
+ * encoding: "utf8",
68
+ * });
69
+ * console.log(result.body); // JSON string
70
+ *
71
+ * @example
72
+ * // Download binary as base64
73
+ * const result = await client.call(["s3", "download"], {
74
+ * bucket: "my-bucket",
75
+ * key: "path/to/image.png",
76
+ * });
77
+ * console.log(result.body); // base64 encoded
78
+ *
79
+ * @example
80
+ * // Download a large object straight to a file
81
+ * await client.call(["s3", "download"], {
82
+ * bucket: "my-bucket",
83
+ * key: "backups/archive.tar.gz",
84
+ * destPath: "/tmp/archive.tar.gz",
85
+ * });
86
+ */
87
+ export async function s3Download(input: S3DownloadInput): Promise<S3DownloadOutput> {
88
+ const client = getS3Client();
89
+
90
+ const command = new GetObjectCommand({
91
+ Bucket: input.bucket,
92
+ Key: input.key,
93
+ VersionId: input.versionId,
94
+ });
95
+
96
+ const response = await client.send(command);
97
+
98
+ // To a file: stream the body, nothing is held in memory (BUGS-2026-07 M22)
99
+ if (input.destPath) {
100
+ if (!response.Body) {
101
+ throw new Error("Empty response body");
102
+ }
103
+ // Write a part file, then rename it. A failed transfer leaves the previous file
104
+ // (createWriteStream on destPath truncated it first: deep dive DATA-14).
105
+ const partPath = `${input.destPath}.${randomUUID().slice(0, 8)}.part`;
106
+ const source = response.Body as Readable;
107
+ try {
108
+ if (input.maxBytes !== undefined) {
109
+ await pipeline(source, byteLimit(input.maxBytes), createWriteStream(partPath));
110
+ } else {
111
+ await pipeline(source, createWriteStream(partPath));
112
+ }
113
+ await rename(partPath, input.destPath);
114
+ } catch (error) {
115
+ await rm(partPath, { force: true });
116
+ throw error;
117
+ }
118
+ return {
119
+ body: "",
120
+ path: input.destPath,
121
+ contentLength: response.ContentLength ?? 0,
122
+ contentType: response.ContentType,
123
+ etag: response.ETag?.replace(/"/g, ""),
124
+ lastModified: response.LastModified?.toISOString(),
125
+ };
126
+ }
127
+
128
+ // Into memory: refuse an object that is too large before reading it (BUGS-2026-07 M22)
129
+ const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
130
+ if (response.ContentLength !== undefined && response.ContentLength > maxBytes) {
131
+ (response.Body as Readable | undefined)?.destroy?.();
132
+ throw tooLarge(response.ContentLength, maxBytes);
133
+ }
134
+
135
+ if (!response.Body) {
136
+ throw new Error("Empty response body");
137
+ }
138
+ const bodyBytes = await readLimited(response.Body as Readable, maxBytes);
139
+
140
+ // Convert to string based on encoding
141
+ let body: string;
142
+ if (input.encoding) {
143
+ body = bodyBytes.toString(input.encoding as BufferEncoding);
144
+ } else {
145
+ body = bodyBytes.toString("base64");
146
+ }
147
+
148
+ return {
149
+ body,
150
+ contentLength: bodyBytes.length,
151
+ contentType: response.ContentType,
152
+ etag: response.ETag?.replace(/"/g, ""),
153
+ lastModified: response.LastModified?.toISOString(),
154
+ };
155
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * S3 Procedures Index
3
+ */
4
+
5
+ export { s3Upload } from "./upload.js";
6
+ export { s3Download } from "./download.js";
7
+ export { s3List } from "./list.js";
8
+ export { s3ListAll } from "./list-all.js";
9
+ export { s3Delete } from "./delete.js";
10
+ export { s3MultipartInit } from "./multipart-init.js";
11
+ export { s3MultipartUpload } from "./multipart-upload.js";
12
+ export { s3MultipartComplete } from "./multipart-complete.js";
13
+ export { s3MultipartAbort } from "./multipart-abort.js";
@@ -0,0 +1,63 @@
1
+ /**
2
+ * s3.listAll procedure
3
+ *
4
+ * List ALL objects in an S3 bucket, transparently following the
5
+ * ListObjectsV2 continuation token so that buckets/prefixes with more than
6
+ * 1000 keys are fully reachable in a single call.
7
+ */
8
+
9
+ import { s3List } from "./list.js";
10
+ import type { S3ListAllInput, S3ListAllOutput, S3ListObject } from "../../types.js";
11
+
12
+ /**
13
+ * List every object under a bucket/prefix, following pagination automatically.
14
+ *
15
+ * Unlike `s3.list` (which returns at most one page — up to 1000 keys — and
16
+ * exposes `nextContinuationToken` for manual paging), `s3.listAll` loops until
17
+ * the result is no longer truncated and returns the aggregated contents.
18
+ *
19
+ * @example
20
+ * const { contents, keyCount } = await client.call(["s3", "listAll"], {
21
+ * bucket: "my-bucket",
22
+ * prefix: "snapshots/",
23
+ * });
24
+ * console.log(keyCount); // e.g. 4213, not clamped to 1000
25
+ */
26
+ export async function s3ListAll(input: S3ListAllInput): Promise<S3ListAllOutput> {
27
+ const contents: S3ListObject[] = [];
28
+ const commonPrefixes: string[] = [];
29
+ const seenPrefixes = new Set<string>();
30
+ let continuationToken: string | undefined = undefined;
31
+ let pageCount = 0;
32
+
33
+ do {
34
+ const page = await s3List({
35
+ bucket: input.bucket,
36
+ prefix: input.prefix,
37
+ delimiter: input.delimiter,
38
+ maxKeys: input.maxKeys,
39
+ continuationToken,
40
+ });
41
+ pageCount += 1;
42
+
43
+ for (const object of page.contents) {
44
+ contents.push(object);
45
+ }
46
+ for (const prefix of page.commonPrefixes) {
47
+ if (!seenPrefixes.has(prefix)) {
48
+ seenPrefixes.add(prefix);
49
+ commonPrefixes.push(prefix);
50
+ }
51
+ }
52
+
53
+ // Only continue while S3 reports more results AND hands back a token.
54
+ continuationToken = page.isTruncated ? page.nextContinuationToken : undefined;
55
+ } while (continuationToken);
56
+
57
+ return {
58
+ contents,
59
+ commonPrefixes,
60
+ keyCount: contents.length,
61
+ pageCount,
62
+ };
63
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * s3.list procedure
3
+ *
4
+ * List objects in S3 bucket.
5
+ */
6
+
7
+ import { ListObjectsV2Command } from "@aws-sdk/client-s3";
8
+ import { getS3Client } from "../../s3-client.js";
9
+ import type { S3ListInput, S3ListOutput, S3ListObject } from "../../types.js";
10
+
11
+ /**
12
+ * List objects in S3 bucket
13
+ *
14
+ * @example
15
+ * // List all objects with prefix
16
+ * const result = await client.call(["s3", "list"], {
17
+ * bucket: "my-bucket",
18
+ * prefix: "snapshots/",
19
+ * maxKeys: 100,
20
+ * });
21
+ *
22
+ * @example
23
+ * // Hierarchical listing with delimiter
24
+ * const result = await client.call(["s3", "list"], {
25
+ * bucket: "my-bucket",
26
+ * prefix: "data/",
27
+ * delimiter: "/",
28
+ * });
29
+ * console.log(result.commonPrefixes); // ["data/2024/", "data/2025/"]
30
+ */
31
+ export async function s3List(input: S3ListInput): Promise<S3ListOutput> {
32
+ const client = getS3Client();
33
+
34
+ const command = new ListObjectsV2Command({
35
+ Bucket: input.bucket,
36
+ Prefix: input.prefix,
37
+ MaxKeys: input.maxKeys || 1000,
38
+ ContinuationToken: input.continuationToken,
39
+ Delimiter: input.delimiter,
40
+ });
41
+
42
+ const response = await client.send(command);
43
+
44
+ const contents: S3ListObject[] = (response.Contents || []).map((obj) => ({
45
+ key: obj.Key || "",
46
+ size: obj.Size || 0,
47
+ lastModified: obj.LastModified?.toISOString() || "",
48
+ etag: obj.ETag?.replace(/"/g, "") || "",
49
+ storageClass: obj.StorageClass,
50
+ }));
51
+
52
+ const commonPrefixes = (response.CommonPrefixes || [])
53
+ .map((cp) => cp.Prefix || "")
54
+ .filter(Boolean);
55
+
56
+ return {
57
+ contents,
58
+ commonPrefixes,
59
+ isTruncated: response.IsTruncated || false,
60
+ nextContinuationToken: response.NextContinuationToken,
61
+ keyCount: response.KeyCount || contents.length,
62
+ };
63
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * s3.multipart.abort procedure
3
+ *
4
+ * Abort a multipart upload and clean up parts.
5
+ */
6
+
7
+ import { AbortMultipartUploadCommand } from "@aws-sdk/client-s3";
8
+ import { getS3Client } from "../../s3-client.js";
9
+ import type { S3MultipartAbortInput, S3MultipartAbortOutput } from "../../types.js";
10
+
11
+ /**
12
+ * Abort multipart upload
13
+ *
14
+ * Use to cancel an in-progress multipart upload.
15
+ * This cleans up any parts that were already uploaded.
16
+ *
17
+ * @example
18
+ * await client.call(["s3", "multipart", "abort"], {
19
+ * bucket: "my-bucket",
20
+ * key: "large-file.tar.gz",
21
+ * uploadId: "abc123",
22
+ * });
23
+ */
24
+ export async function s3MultipartAbort(
25
+ input: S3MultipartAbortInput
26
+ ): Promise<S3MultipartAbortOutput> {
27
+ const client = getS3Client();
28
+
29
+ const command = new AbortMultipartUploadCommand({
30
+ Bucket: input.bucket,
31
+ Key: input.key,
32
+ UploadId: input.uploadId,
33
+ });
34
+
35
+ await client.send(command);
36
+
37
+ return {
38
+ aborted: true,
39
+ };
40
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * s3.multipart.complete procedure
3
+ *
4
+ * Complete a multipart upload by assembling parts.
5
+ */
6
+
7
+ import { CompleteMultipartUploadCommand } from "@aws-sdk/client-s3";
8
+ import { getS3Client } from "../../s3-client.js";
9
+ import type { S3MultipartCompleteInput, S3MultipartCompleteOutput } from "../../types.js";
10
+
11
+ /**
12
+ * Complete multipart upload
13
+ *
14
+ * Call after all parts have been uploaded. Parts must be provided
15
+ * in order with their ETags from the upload responses.
16
+ *
17
+ * @example
18
+ * const result = await client.call(["s3", "multipart", "complete"], {
19
+ * bucket: "my-bucket",
20
+ * key: "large-file.tar.gz",
21
+ * uploadId: "abc123",
22
+ * parts: [
23
+ * { partNumber: 1, etag: "etag1" },
24
+ * { partNumber: 2, etag: "etag2" },
25
+ * { partNumber: 3, etag: "etag3" },
26
+ * ],
27
+ * });
28
+ */
29
+ export async function s3MultipartComplete(
30
+ input: S3MultipartCompleteInput
31
+ ): Promise<S3MultipartCompleteOutput> {
32
+ const client = getS3Client();
33
+
34
+ const command = new CompleteMultipartUploadCommand({
35
+ Bucket: input.bucket,
36
+ Key: input.key,
37
+ UploadId: input.uploadId,
38
+ MultipartUpload: {
39
+ // S3 refuses parts out of order. Parts that were uploaded in parallel can arrive in any
40
+ // order, so the procedure sorts them by number.
41
+ Parts: [...input.parts]
42
+ .sort((a, b) => a.partNumber - b.partNumber)
43
+ .map((part) => ({
44
+ ETag: `"${part.etag}"`,
45
+ PartNumber: part.partNumber,
46
+ })),
47
+ },
48
+ });
49
+
50
+ const response = await client.send(command);
51
+
52
+ return {
53
+ location: response.Location || `s3://${input.bucket}/${input.key}`,
54
+ bucket: response.Bucket || input.bucket,
55
+ key: response.Key || input.key,
56
+ etag: response.ETag?.replace(/"/g, "") || "",
57
+ };
58
+ }