From 22b064c437726a2c2467ffb02179e2d998da6c5f Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:44:12 +0000 Subject: [PATCH 1/2] feat(core): add S3 wire checksum download stream --- .../src/submodules/serde/index.browser.ts | 5 + .../core/src/submodules/serde/index.native.ts | 5 + packages/core/src/submodules/serde/index.ts | 2 + .../checksum/wire/AwsChunkedDecoder.ts | 213 ++++++++++++++++++ .../wire/WireChecksumStream.browser.ts | 48 ++++ .../checksum/wire/WireChecksumStream.ts | 192 ++++++++++++++++ .../wire/createWireChecksumStream.browser.ts | 124 ++++++++++ .../checksum/wire/createWireChecksumStream.ts | 32 +++ 8 files changed, 621 insertions(+) create mode 100644 packages/core/src/submodules/serde/util-stream/checksum/wire/AwsChunkedDecoder.ts create mode 100644 packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.browser.ts create mode 100644 packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.ts create mode 100644 packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.browser.ts create mode 100644 packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.ts diff --git a/packages/core/src/submodules/serde/index.browser.ts b/packages/core/src/submodules/serde/index.browser.ts index a446e29bd63..2fbfc8218cb 100644 --- a/packages/core/src/submodules/serde/index.browser.ts +++ b/packages/core/src/submodules/serde/index.browser.ts @@ -92,6 +92,11 @@ export const Hash = no; export class Uint8ArrayBlobAdapter extends bindUint8ArrayBlobAdapter(toUtf8, fromUtf8, toBase64, fromBase64) {} export { ChecksumStream, type ChecksumStreamInit } from "./util-stream/checksum/ChecksumStream.browser"; export { createChecksumStream } from "./util-stream/checksum/createChecksumStream.browser"; +export { + WireChecksumStream, + type WireChecksumStreamInit, +} from "./util-stream/checksum/wire/WireChecksumStream.browser"; +export { createWireChecksumStream } from "./util-stream/checksum/wire/createWireChecksumStream.browser"; export { createBufferedReadable } from "./util-stream/createBufferedReadable.browser"; export { getAwsChunkedEncodingStream } from "./util-stream/getAwsChunkedEncodingStream.browser"; export { headStream } from "./util-stream/headStream.browser"; diff --git a/packages/core/src/submodules/serde/index.native.ts b/packages/core/src/submodules/serde/index.native.ts index 2e474ca7a4f..ae145beddf3 100644 --- a/packages/core/src/submodules/serde/index.native.ts +++ b/packages/core/src/submodules/serde/index.native.ts @@ -94,6 +94,11 @@ export const Hash = no; export class Uint8ArrayBlobAdapter extends bindUint8ArrayBlobAdapter(toUtf8, fromUtf8, toBase64, fromBase64) {} export { ChecksumStream, type ChecksumStreamInit } from "./util-stream/checksum/ChecksumStream.browser"; export { createChecksumStream } from "./util-stream/checksum/createChecksumStream.browser"; +export { + WireChecksumStream, + type WireChecksumStreamInit, +} from "./util-stream/checksum/wire/WireChecksumStream.browser"; +export { createWireChecksumStream } from "./util-stream/checksum/wire/createWireChecksumStream.browser"; export { createBufferedReadable } from "./util-stream/createBufferedReadable.browser"; export { getAwsChunkedEncodingStream } from "./util-stream/getAwsChunkedEncodingStream.browser"; export { headStream } from "./util-stream/headStream.browser"; diff --git a/packages/core/src/submodules/serde/index.ts b/packages/core/src/submodules/serde/index.ts index 12db0547977..53d447840f1 100644 --- a/packages/core/src/submodules/serde/index.ts +++ b/packages/core/src/submodules/serde/index.ts @@ -90,6 +90,8 @@ export { Hash } from "./hash-node/hash-node"; export class Uint8ArrayBlobAdapter extends bindUint8ArrayBlobAdapter(toUtf8, fromUtf8, toBase64, fromBase64) {} export { ChecksumStream, type ChecksumStreamInit } from "./util-stream/checksum/ChecksumStream"; export { createChecksumStream } from "./util-stream/checksum/createChecksumStream"; +export { WireChecksumStream, type WireChecksumStreamInit } from "./util-stream/checksum/wire/WireChecksumStream"; +export { createWireChecksumStream } from "./util-stream/checksum/wire/createWireChecksumStream"; export { createBufferedReadable } from "./util-stream/createBufferedReadable"; export { getAwsChunkedEncodingStream } from "./util-stream/getAwsChunkedEncodingStream"; export { headStream } from "./util-stream/headStream"; diff --git a/packages/core/src/submodules/serde/util-stream/checksum/wire/AwsChunkedDecoder.ts b/packages/core/src/submodules/serde/util-stream/checksum/wire/AwsChunkedDecoder.ts new file mode 100644 index 00000000000..04bf5c9f69a --- /dev/null +++ b/packages/core/src/submodules/serde/util-stream/checksum/wire/AwsChunkedDecoder.ts @@ -0,0 +1,213 @@ +/** + * Handlers invoked by {@link AwsChunkedDecoder} as it decodes an + * `aws-chunked` encoded stream. + * + * @internal + */ +export interface AwsChunkedDecoderHandlers { + /** + * Called with each segment of decoded data bytes (framing stripped). + * May be called multiple times per {@link AwsChunkedDecoder.write}. + */ + onData(chunk: Uint8Array): void; + /** + * Called once per trailer line found after the terminal chunk. + * @param name - the trailer field name (as received, not normalized). + * @param value - the trailer field value. + */ + onTrailer(name: string, value: string): void; +} + +const CR = 0x0d; // \r +const LF = 0x0a; // \n + +type State = "CHUNK_HEADER" | "CHUNK_DATA" | "CHUNK_DATA_CRLF" | "TRAILER" | "DONE"; + +/** + * Incremental decoder for `aws-chunked` encoded response bodies. + * + * Data is fed in arbitrarily sized segments via {@link write}. Decoded data + * bytes are surfaced through {@link AwsChunkedDecoderHandlers.onData} as soon + * as they are available - the decoder never buffers the full payload. Trailer + * fields appended after the terminal chunk are surfaced through + * {@link AwsChunkedDecoderHandlers.onTrailer}. + * + * Wire format (see the S3 Wire Checksum spec): + * ``` + * [;chunk-ext]\r\n\r\n (repeated) + * 0[;chunk-ext]\r\n (terminal chunk, size 0) + * : \r\n (zero or more trailers) + * \r\n (end of trailers) + * ``` + * + * The chunk extension (e.g. `;chunk-signature=UNSIGNED-PAYLOAD`) is parsed and + * ignored - no assumption is made about its value, for forward compatibility. + * + * @internal + */ +export class AwsChunkedDecoder { + private state: State = "CHUNK_HEADER"; + /** + * Accumulates bytes of the current control line (chunk header or trailer). + * Data bytes are never accumulated here - they are emitted directly. + */ + private lineBuffer: number[] = []; + /** + * Remaining number of data bytes to read for the current chunk. + */ + private chunkRemaining = 0; + /** + * Remaining number of CRLF bytes to consume after a chunk's data. + */ + private crlfRemaining = 0; + /** + * Total number of decoded data bytes emitted so far. + */ + private decodedByteCount = 0; + + public constructor(private readonly handlers: AwsChunkedDecoderHandlers) {} + + /** + * The number of decoded data bytes emitted so far. After the stream is fully + * decoded this equals the `x-amz-decoded-content-length`. + */ + public get bytesDecoded(): number { + return this.decodedByteCount; + } + + /** + * Feed a segment of encoded bytes into the decoder. + * @param input - a slice of the aws-chunked encoded stream. + */ + public write(input: Uint8Array): void { + let offset = 0; + const length = input.length; + + while (offset < length) { + switch (this.state) { + case "CHUNK_HEADER": { + while (offset < length) { + const byte = input[offset++]; + if (byte === LF) { + this.parseChunkHeader(); + break; + } else if (byte !== CR) { + this.lineBuffer.push(byte); + } + } + break; + } + case "CHUNK_DATA": { + const take = Math.min(this.chunkRemaining, length - offset); + if (take > 0) { + const data = input.subarray(offset, offset + take); + this.decodedByteCount += take; + this.chunkRemaining -= take; + offset += take; + this.handlers.onData(data); + } + if (this.chunkRemaining === 0) { + this.state = "CHUNK_DATA_CRLF"; + this.crlfRemaining = 2; + } + break; + } + case "CHUNK_DATA_CRLF": { + while (offset < length && this.crlfRemaining > 0) { + const byte = input[offset++]; + if (byte !== CR && byte !== LF) { + throw new Error("@smithy/util-stream: aws-chunked decode error - expected CRLF after chunk data."); + } + this.crlfRemaining--; + } + if (this.crlfRemaining === 0) { + this.state = "CHUNK_HEADER"; + } + break; + } + case "TRAILER": { + while (offset < length) { + const byte = input[offset++]; + if (byte === LF) { + if (this.parseTrailerLine()) { + this.state = "DONE"; + } + break; + } else if (byte !== CR) { + this.lineBuffer.push(byte); + } + } + break; + } + case "DONE": { + // Ignore any bytes received after the trailer terminator. + offset = length; + break; + } + } + } + } + + /** + * Signal that the upstream source has ended. Throws if the stream ended + * before the terminal chunk and trailers were fully received. + */ + public end(): void { + if (this.state !== "DONE") { + throw new Error( + "@smithy/util-stream: aws-chunked decode error - stream ended before the terminal chunk and trailers were received." + ); + } + } + + /** + * Parse the accumulated chunk header line and transition state. + */ + private parseChunkHeader(): void { + const line = this.takeLine(); + // Strip any chunk extension (e.g. ";chunk-signature=UNSIGNED-PAYLOAD"). + const semicolon = line.indexOf(";"); + const hex = (semicolon === -1 ? line : line.slice(0, semicolon)).trim(); + + if (hex.length === 0 || !/^[0-9a-fA-F]+$/.test(hex)) { + throw new Error(`@smithy/util-stream: aws-chunked decode error - invalid chunk size "${hex}".`); + } + + const size = parseInt(hex, 16); + if (size === 0) { + this.state = "TRAILER"; + } else { + this.chunkRemaining = size; + this.state = "CHUNK_DATA"; + } + } + + /** + * Parse the accumulated trailer line. + * @returns true when the trailer section is terminated (empty line). + */ + private parseTrailerLine(): boolean { + const line = this.takeLine(); + if (line.length === 0) { + return true; + } + const separator = line.indexOf(":"); + if (separator === -1) { + throw new Error(`@smithy/util-stream: aws-chunked decode error - malformed trailer "${line}".`); + } + const name = line.slice(0, separator).trim(); + const value = line.slice(separator + 1).trim(); + this.handlers.onTrailer(name, value); + return false; + } + + /** + * Consume and return the accumulated control line as a string, resetting the + * line buffer. + */ + private takeLine(): string { + const line = String.fromCharCode(...this.lineBuffer); + this.lineBuffer = []; + return line; + } +} diff --git a/packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.browser.ts b/packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.browser.ts new file mode 100644 index 00000000000..d0022750e8c --- /dev/null +++ b/packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.browser.ts @@ -0,0 +1,48 @@ +import type { Checksum, Encoder } from "@smithy/types"; + +/** + * @internal + */ +export interface WireChecksumStreamInit { + /** + * The wire checksum algorithm name (e.g. `crc32`), as reported by the + * `x-amz-wire-checksum-algorithm` response header. Used to derive the + * trailer field name (`x-amz-wire-checksum-`). + */ + checksumAlgorithm: string; + + /** + * The checksum calculator. Updated incrementally over the decoded data bytes. + */ + checksum: Checksum; + + /** + * The `aws-chunked` encoded stream carrying the response body. The framing is + * decoded before being surfaced, and the checksum value is read from the + * trailer. + */ + source: ReadableStream; + + /** + * The `x-amz-decoded-content-length` value. When provided, the total decoded + * byte count is verified against it. A mismatch indicates a truncated or + * corrupt response. + */ + decodedContentLength?: number; + + /** + * Optional base 64 encoder if calling from a request context. + */ + base64Encoder?: Encoder; +} + +const ReadableStreamRef = typeof ReadableStream === "function" ? ReadableStream : function (): void {}; + +/** + * This stub exists so that the readable returned by createWireChecksumStream + * identifies as "WireChecksumStream" in alignment with the Node.js + * implementation. + * + * @extends ReadableStream + */ +export class WireChecksumStream extends (ReadableStreamRef as any) {} diff --git a/packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.ts b/packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.ts new file mode 100644 index 00000000000..1ad0c01d66a --- /dev/null +++ b/packages/core/src/submodules/serde/util-stream/checksum/wire/WireChecksumStream.ts @@ -0,0 +1,192 @@ +import { Duplex, type Readable } from "node:stream"; +import type { Checksum, Encoder } from "@smithy/types"; + +import { toBase64 } from "../../../util-base64/toBase64"; +import { AwsChunkedDecoder } from "./AwsChunkedDecoder"; + +/** + * @internal + */ +export interface WireChecksumStreamInit { + /** + * The wire checksum algorithm name (e.g. `crc32`), as reported by the + * `x-amz-wire-checksum-algorithm` response header. Used to derive the + * trailer field name (`x-amz-wire-checksum-`). + */ + checksumAlgorithm: string; + + /** + * The checksum calculator. Updated incrementally over the decoded data bytes. + */ + checksum: Checksum; + + /** + * The `aws-chunked` encoded stream carrying the response body. The framing is + * decoded before being surfaced, and the checksum value is read from the + * trailer. + */ + source: T; + + /** + * The `x-amz-decoded-content-length` value. When provided, the total decoded + * byte count is verified against it. A mismatch indicates a truncated or + * corrupt response. + */ + decodedContentLength?: number; + + /** + * Optional base 64 encoder if calling from a request context. + */ + base64Encoder?: Encoder; +} + +/** + * Stream wrapper that validates an S3 wire checksum without buffering the full + * response. It mirrors its source stream's interface. + * + * It decodes the `aws-chunked` framing so the caller only sees the decoded + * object data, and reads the expected checksum from the trailer once the body + * is fully consumed. + * + * @internal + */ +export class WireChecksumStream extends Duplex { + private readonly checksum: Checksum; + private readonly base64Encoder: Encoder; + private readonly trailerName: string; + private readonly decodedContentLength?: number; + private readonly decoder: AwsChunkedDecoder; + + private source?: Readable; + private expectedChecksum?: string; + private canPushMore = true; + private pendingCallback: ((err?: Error) => void) | null = null; + + public constructor({ + checksumAlgorithm, + checksum, + source, + decodedContentLength, + base64Encoder, + }: WireChecksumStreamInit) { + super(); + if (typeof (source as Readable).pipe === "function") { + this.source = source as Readable; + } else { + throw new Error( + `@smithy/util-stream: unsupported source type ${source?.constructor?.name ?? source} in WireChecksumStream.` + ); + } + + this.checksum = checksum; + this.decodedContentLength = decodedContentLength; + this.base64Encoder = base64Encoder ?? toBase64; + this.trailerName = `x-amz-wire-checksum-${checksumAlgorithm.toLowerCase()}`; + + this.decoder = new AwsChunkedDecoder({ + onData: (data) => { + this.checksum.update(data); + if (!this.push(data)) { + this.canPushMore = false; + } + }, + onTrailer: (name, value) => { + if (name.toLowerCase() === this.trailerName) { + this.expectedChecksum = value; + } + }, + }); + + // connect this stream to the end of the source stream. + this.source.pipe(this); + } + + /** + * Do not call this directly. + * @internal + */ + public _read( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + size: number + ): void { + if (this.pendingCallback) { + const callback = this.pendingCallback; + this.pendingCallback = null; + callback(); + } + } + + /** + * When the upstream source flows data to this stream, decode the `aws-chunked` + * framing and calculate a step update of the checksum over the decoded bytes. + * Do not call this directly. + * @internal + */ + public _write(chunk: Buffer, encoding: string, callback: (err?: Error) => void): void { + try { + this.canPushMore = true; + // onData updates the checksum and pushes decoded bytes downstream. + this.decoder.write(chunk); + if (!this.canPushMore) { + this.pendingCallback = callback; + return; + } + } catch (e: unknown) { + return callback(e as Error); + } + return callback(); + } + + /** + * When the upstream source finishes, finalize decoding and perform the + * checksum comparison against the value read from the trailer. + * Do not call this directly. + * @internal + */ + public async _final(callback: (err?: Error) => void): Promise { + try { + this.decoder.end(); + if (this.decodedContentLength !== undefined && this.decoder.bytesDecoded !== this.decodedContentLength) { + return callback( + new Error( + `@smithy/util-stream: decoded content length mismatch: expected ${this.decodedContentLength}` + + ` but decoded ${this.decoder.bytesDecoded} bytes from the aws-chunked response.` + ) + ); + } + if (this.expectedChecksum === undefined) { + return callback( + new Error(`@smithy/util-stream: wire checksum trailer "${this.trailerName}" was not present in the response.`) + ); + } + + const digest: Uint8Array = await this.checksum.digest(); + const received = this.base64Encoder(digest); + if (this.expectedChecksum !== received) { + return callback( + new Error( + `Checksum mismatch: expected "${this.expectedChecksum}" but received "${received}"` + + ` in response trailer "${this.trailerName}".` + ) + ); + } + } catch (e: unknown) { + return callback(e as Error); + } + this.push(null); + return callback(); + } + + /** + * Destroy the upstream source for cleanup so it is not left dangling, then + * complete this stream's destruction. The error is intentionally not forwarded + * to the source as the source is typically internal and without an error listener. + * The error still surfaces on this stream via the callback. + * Do not call this directly. + * @internal + */ + public _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): void { + this.source?.destroy(); + callback(error); + } +} diff --git a/packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.browser.ts b/packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.browser.ts new file mode 100644 index 00000000000..38404b63070 --- /dev/null +++ b/packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.browser.ts @@ -0,0 +1,124 @@ +import { toBase64 } from "../../../util-base64/toBase64.browser"; +import { isReadableStream } from "../../stream-type-check"; +import { AwsChunkedDecoder } from "./AwsChunkedDecoder"; +import { WireChecksumStream, type WireChecksumStreamInit } from "./WireChecksumStream.browser"; + +/** + * Alias prevents compiler from turning + * ReadableStream into ReadableStream, which is incompatible + * with the NodeJS.ReadableStream global type. + * @internal + */ +export type ReadableStreamType = ReadableStream; + +/** + * This is a local copy of + * https://developer.mozilla.org/en-US/docs/Web/API/TransformStreamDefaultController + * in case users do not have this type. + */ +interface TransformStreamDefaultController { + enqueue(chunk: any): void; + error(error: unknown): void; + terminate(): void; +} + +/** + * Creates a stream adapter that validates an S3 wire checksum for streams + * without buffering the full payload. It decodes the `aws-chunked` framing so + * the caller only sees decoded object data, and reads the expected checksum + * from the trailer. + * @internal + */ +export const createWireChecksumStream = ({ + checksumAlgorithm, + checksum, + source, + decodedContentLength, + base64Encoder, +}: WireChecksumStreamInit): ReadableStreamType => { + if (!isReadableStream(source)) { + throw new Error( + `@smithy/util-stream: unsupported source type ${ + (source as any)?.constructor?.name ?? source + } in WireChecksumStream.` + ); + } + + if (typeof TransformStream !== "function") { + throw new Error( + "@smithy/util-stream: unable to instantiate WireChecksumStream because API unavailable: ReadableStream/TransformStream." + ); + } + + const encoder = base64Encoder ?? toBase64; + const trailerName = `x-amz-wire-checksum-${checksumAlgorithm.toLowerCase()}`; + + // The expected value is read from the trailer during decoding. + let expectedChecksumValue: string | undefined; + + // The controller is supplied per transform() call; track the active one so + // the decoder handlers (created once) can enqueue against it. + let activeController: TransformStreamDefaultController | undefined; + const decoder = new AwsChunkedDecoder({ + onData(data: Uint8Array) { + checksum.update(data); + activeController!.enqueue(data); + }, + onTrailer(name: string, value: string) { + if (name.toLowerCase() === trailerName) { + expectedChecksumValue = value; + } + }, + }); + + const transform = new TransformStream({ + start() {}, + async transform(chunk: any, controller: TransformStreamDefaultController) { + activeController = controller; + // onData updates the checksum and enqueues the decoded bytes. + decoder.write(chunk); + }, + async flush(controller: TransformStreamDefaultController) { + try { + decoder.end(); + } catch (e: unknown) { + controller.error(e); + return; + } + if (decodedContentLength !== undefined && decoder.bytesDecoded !== decodedContentLength) { + controller.error( + new Error( + `@smithy/util-stream: decoded content length mismatch: expected ${decodedContentLength}` + + ` but decoded ${decoder.bytesDecoded} bytes from the aws-chunked response.` + ) + ); + return; + } + if (expectedChecksumValue === undefined) { + controller.error( + new Error(`@smithy/util-stream: wire checksum trailer "${trailerName}" was not present in the response.`) + ); + return; + } + + const digest: Uint8Array = await checksum.digest(); + const received = encoder(digest); + + if (expectedChecksumValue !== received) { + controller.error( + new Error( + `Checksum mismatch: expected "${expectedChecksumValue}" but received "${received}"` + + ` in response trailer "${trailerName}".` + ) + ); + } else { + controller.terminate(); + } + }, + }); + + source.pipeThrough(transform); + const readable = transform.readable; + Object.setPrototypeOf(readable, WireChecksumStream.prototype); + return readable; +}; diff --git a/packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.ts b/packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.ts new file mode 100644 index 00000000000..346e5f3e70e --- /dev/null +++ b/packages/core/src/submodules/serde/util-stream/checksum/wire/createWireChecksumStream.ts @@ -0,0 +1,32 @@ +import type { Readable } from "node:stream"; + +import { isReadableStream } from "../../stream-type-check"; +import { WireChecksumStream, type WireChecksumStreamInit } from "./WireChecksumStream"; +import { + createWireChecksumStream as createWireChecksumStreamWeb, + type ReadableStreamType, +} from "./createWireChecksumStream.browser"; + +/** + * Creates a stream mirroring the input stream's interface that validates an S3 + * wire checksum when reading to the end of the stream. It decodes the + * `aws-chunked` framing so the caller only sees the decoded object data, and + * reads the expected checksum from the trailer. + * @internal + */ +export function createWireChecksumStream(init: WireChecksumStreamInit): ReadableStreamType; +/** + * @internal + */ +export function createWireChecksumStream(init: WireChecksumStreamInit): Readable; +/** + * @internal + */ +export function createWireChecksumStream( + init: WireChecksumStreamInit +): Readable | ReadableStreamType { + if (typeof ReadableStream === "function" && isReadableStream(init.source)) { + return createWireChecksumStreamWeb(init as WireChecksumStreamInit); + } + return new WireChecksumStream(init as WireChecksumStreamInit); +} From 81f9eed9d40adabae63290b069ad34482cad2008 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:18:48 +0000 Subject: [PATCH 2/2] chore: yarn changeset --- .changeset/stale-rockets-heal.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stale-rockets-heal.md diff --git a/.changeset/stale-rockets-heal.md b/.changeset/stale-rockets-heal.md new file mode 100644 index 00000000000..689e7ec02a5 --- /dev/null +++ b/.changeset/stale-rockets-heal.md @@ -0,0 +1,5 @@ +--- +"@smithy/core": minor +--- + +Add createWireChecksumStream to validate S3 wire checksums delivered in the aws-chunked response trailer.