ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.