AWS SDK v3 S3 storage helpers tested with aws-sdk-client-mock: working versions (tested 2026-10-15)
S3 helpers on @aws-sdk/client-s3 for JSON put/get with NoSuchKey handling, paginated listing, batched DeleteObjects and locally signed presigned URLs, tested offline with aws-sdk-client-mock and sdkStreamMixin bodies.
passed5 passed · 0 failed2026-10-15 14:56 UTC- Status
- passed
- Last passed
- 2026-10-15 14:56 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions6
@aws-sdk/client-s3@3.1152.0@aws-sdk/s3-request-presigner@3.1152.0aws-sdk-client-mock@4.1.0@smithy/util-stream@4.8.2tsx@4.23.15@types/node@26.6.5
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38755782594
The code
Exactly the files that passed. Your agent can pull them with get_recipe("aws-sdk-v3-s3-client-mock") (install), or see them on GitHub.
package.json
{
"name": "aws-sdk-v3-s3-client-mock",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1152.0",
"@aws-sdk/s3-request-presigner": "3.1152.0"
},
"devDependencies": {
"aws-sdk-client-mock": "4.1.0",
"@smithy/util-stream": "4.8.2",
"tsx": "4.23.15",
"@types/node": "26.6.5"
}
}
src/storage.ts
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectsCommand, paginateListObjectsV2, NoSuchKey } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export function createS3(region = "us-east-1") {
// Static fake credentials keep presigning and tests fully offline; real code would use the default chain.
return new S3Client({ region, credentials: { accessKeyId: "AKIDEXAMPLE", secretAccessKey: "secret" } });
}
export class Storage {
constructor(private s3: S3Client, private bucket: string) {}
async putJson(key: string, value: unknown) {
const res = await this.s3.send(new PutObjectCommand({
Bucket: this.bucket, Key: key, Body: JSON.stringify(value), ContentType: "application/json", ServerSideEncryption: "AES256",
}));
return res.ETag;
}
/** Returns null for a missing key instead of throwing. */
async getJson<T>(key: string): Promise<T | null> {
try {
const res = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
return JSON.parse(await res.Body!.transformToString()) as T;
} catch (e) {
if (e instanceof NoSuchKey) return null;
throw e;
}
}
/** Walks every page of ListObjectsV2 with the built-in paginator. */
async listKeys(prefix: string): Promise<string[]> {
const keys: string[] = [];
for await (const page of paginateListObjectsV2({ client: this.s3, pageSize: 2 }, { Bucket: this.bucket, Prefix: prefix })) {
for (const o of page.Contents ?? []) keys.push(o.Key!);
}
return keys;
}
/** Deletes in chunks of 1000, the DeleteObjects limit, and returns any per-key errors. */
async deleteKeys(keys: string[]) {
const errors: string[] = [];
for (let i = 0; i < keys.length; i += 1000) {
const res = await this.s3.send(new DeleteObjectsCommand({ Bucket: this.bucket, Delete: { Objects: keys.slice(i, i + 1000).map((Key) => ({ Key })) } }));
for (const e of res.Errors ?? []) errors.push(`${e.Key}: ${e.Code}`);
}
return errors;
}
/** Presigned GET URL, signed locally with no network call. */
downloadUrl(key: string, expiresIn = 300) {
return getSignedUrl(this.s3, new GetObjectCommand({ Bucket: this.bucket, Key: key }), { expiresIn });
}
}
test/storage.test.ts
import { test, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { Readable } from "node:stream";
import { mockClient } from "aws-sdk-client-mock";
import { sdkStreamMixin } from "@smithy/util-stream";
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, NoSuchKey } from "@aws-sdk/client-s3";
import { Storage, createS3 } from "../src/storage.ts";
const s3Mock = mockClient(S3Client);
beforeEach(() => s3Mock.reset());
const storage = () => new Storage(createS3(), "uploads");
test("putJson sends the right command input", async () => {
s3Mock.on(PutObjectCommand).resolves({ ETag: '"abc"' });
assert.equal(await storage().putJson("a.json", { n: 1 }), '"abc"');
const [call] = s3Mock.commandCalls(PutObjectCommand);
assert.deepEqual(call.args[0].input, { Bucket: "uploads", Key: "a.json", Body: '{"n":1}', ContentType: "application/json", ServerSideEncryption: "AES256" });
});
test("getJson reads a streamed body and maps NoSuchKey to null", async () => {
s3Mock.on(GetObjectCommand, { Key: "cfg.json" }).resolves({ Body: sdkStreamMixin(Readable.from([Buffer.from('{"theme":"dark"}')])) });
s3Mock.on(GetObjectCommand, { Key: "gone.json" }).rejects(new NoSuchKey({ message: "missing", $metadata: {} }));
assert.deepEqual(await storage().getJson("cfg.json"), { theme: "dark" });
assert.equal(await storage().getJson("gone.json"), null);
});
test("listKeys follows continuation tokens across pages", async () => {
s3Mock.on(ListObjectsV2Command)
.resolvesOnce({ Contents: [{ Key: "img/1.png" }, { Key: "img/2.png" }], IsTruncated: true, NextContinuationToken: "t1" })
.resolvesOnce({ Contents: [{ Key: "img/3.png" }], IsTruncated: false });
assert.deepEqual(await storage().listKeys("img/"), ["img/1.png", "img/2.png", "img/3.png"]);
const calls = s3Mock.commandCalls(ListObjectsV2Command);
assert.equal(calls.length, 2);
assert.equal(calls[1].args[0].input.ContinuationToken, "t1");
assert.equal(calls[0].args[0].input.MaxKeys, 2);
});
test("deleteKeys batches by 1000 and surfaces per-key errors", async () => {
s3Mock.on(DeleteObjectsCommand).resolvesOnce({}).resolvesOnce({ Errors: [{ Key: "k1000", Code: "AccessDenied" }] });
const keys = Array.from({ length: 1001 }, (_, i) => `k${i}`);
assert.deepEqual(await storage().deleteKeys(keys), ["k1000: AccessDenied"]);
const calls = s3Mock.commandCalls(DeleteObjectsCommand);
assert.deepEqual(calls.map((c) => c.args[0].input.Delete!.Objects!.length), [1000, 1]);
});
test("presigned URLs are signed locally with SigV4 query params", async () => {
const url = new URL(await storage().downloadUrl("reports/q3.pdf", 600));
assert.equal(url.hostname, "uploads.s3.us-east-1.amazonaws.com");
assert.equal(url.pathname, "/reports/q3.pdf");
assert.equal(url.searchParams.get("X-Amz-Algorithm"), "AWS4-HMAC-SHA256");
assert.equal(url.searchParams.get("X-Amz-Expires"), "600");
assert.match(url.searchParams.get("X-Amz-Credential")!, /^AKIDEXAMPLE\/\d{8}\/us-east-1\/s3\/aws4_request$/);
assert.equal(s3Mock.calls().length, 0); // presigning never sends a request
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.