diff --git a/jest.config.js b/jest.config.js index cc3ea14..3457df9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { moduleNameMapper: { "^@microsoft/durabletask-js$": "/packages/durabletask-js/src/index.ts", "^@microsoft/durabletask-js-azuremanaged$": "/packages/durabletask-js-azuremanaged/src/index.ts", + "^@microsoft/durabletask-js-azureblob$": "/packages/durabletask-js-azureblob/src/index.ts", "^@microsoft/durabletask-js-export-history$": "/packages/durabletask-js-export-history/src/index.ts", }, transform: { diff --git a/package-lock.json b/package-lock.json index 2922319..7878951 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1630,6 +1630,10 @@ "resolved": "packages/durabletask-js", "link": true }, + "node_modules/@microsoft/durabletask-js-azureblob": { + "resolved": "packages/durabletask-js-azureblob", + "link": true + }, "node_modules/@microsoft/durabletask-js-azuremanaged": { "resolved": "packages/durabletask-js-azuremanaged", "link": true @@ -7651,6 +7655,21 @@ } } }, + "packages/durabletask-js-azureblob": { + "name": "@microsoft/durabletask-js-azureblob", + "version": "0.4.0", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.10.0", + "@azure/storage-blob": "^12.26.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@microsoft/durabletask-js": ">=0.4.1" + } + }, "packages/durabletask-js-azuremanaged": { "name": "@microsoft/durabletask-js-azuremanaged", "version": "0.4.0", diff --git a/packages/durabletask-js-azureblob/CHANGELOG.md b/packages/durabletask-js-azureblob/CHANGELOG.md new file mode 100644 index 0000000..d9b0052 --- /dev/null +++ b/packages/durabletask-js-azureblob/CHANGELOG.md @@ -0,0 +1,11 @@ +## Upcoming + +### New + +- Require core SDK `>=0.4.1`, excluding published `0.4.0`, which does not export the payload APIs. +- Add opt-in Azure Blob payload storage, interoperable with durabletask-dotnet + `91739687ff647699079cdadd627daa00562982b7`: UTF-8/gzip blobs, credential-free v2 tokens, + legacy v1 reads, safe same-service and identity-only cross-account routing. +- Support explicit ownership-checked, ETag-conditional deletion, including snapshots and + confirmed-absence handling. Scheduler-driven cleanup follows in later phases; do not release + partial feature phases. diff --git a/packages/durabletask-js-azureblob/LICENSE b/packages/durabletask-js-azureblob/LICENSE new file mode 100644 index 0000000..22aed37 --- /dev/null +++ b/packages/durabletask-js-azureblob/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/durabletask-js-azureblob/README.md b/packages/durabletask-js-azureblob/README.md new file mode 100644 index 0000000..2430be9 --- /dev/null +++ b/packages/durabletask-js-azureblob/README.md @@ -0,0 +1,111 @@ +# Azure Blob payload storage for Durable Task + +`@microsoft/durabletask-js-azureblob` adds opt-in external payload storage to the +Durable Task JavaScript SDK. Core users do not acquire Azure Storage dependencies. +Its tokens, UTF-8/gzip representation, and ownership metadata +match [durabletask-dotnet at `91739687`](https://github.com/microsoft/durabletask-dotnet/tree/91739687ff647699079cdadd627daa00562982b7/src/Extensions/AzureBlobPayloads). + +This is phase 3 of the six-PR feature stack. The provider works with the preceding SDK +transport integration; scheduler-driven automatic cleanup is implemented in later phases. +Do not publish a partial feature phase. + +## Configure clients and workers + +Requires `@microsoft/durabletask-js >=0.4.1`, the first core source version with +`PayloadStore` and the payload transport APIs. Published core `0.4.0` is incompatible; +npm rejects that peer pairing. Publish the payload-capable core release before this extension. + +```sh +npm install @microsoft/durabletask-js @microsoft/durabletask-js-azureblob +``` + +```typescript +import { TaskHubGrpcClient, TaskHubGrpcWorker } from "@microsoft/durabletask-js"; +import { BlobPayloadStore } from "@microsoft/durabletask-js-azureblob"; + +const store = new BlobPayloadStore({ + connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING!, + // containerName: "durabletask-payloads", + // compressionEnabled: true, +}); +const payloads = { store, thresholdBytes: 256 * 1024, maxPayloadBytes: 10 * 1024 * 1024 }; +const client = new TaskHubGrpcClient({ hostAddress: "localhost:8080", payloads }); +const worker = new TaskHubGrpcWorker({ hostAddress: "localhost:8080", payloads }); +``` + +For identity authentication, install `@azure/identity` and replace the store configuration: + +```typescript +import { DefaultAzureCredential } from "@azure/identity"; + +const store = new BlobPayloadStore({ + accountUri: "https://myaccount.blob.core.windows.net", + credential: new DefaultAzureCredential(), +}); +``` + +The identity needs permission to create the private container and read/write its blobs, +and delete permission when invoking the store's deletion API. The SDK does not provision accounts or roles. +Azure-managed scheduler builders expose `.useExternalizedPayloads(payloads)` on both +`DurableTaskAzureManagedClientBuilder` and `DurableTaskAzureManagedWorkerBuilder`. +Scheduler credentials and blob credentials are independent. + +Configure every participating client and worker, including .NET workers in a mixed-language +task hub, **before** writing externalized payloads. Workers advertise large-payload capability +only when enabled. Removing support while stored tokens remain makes those instances unreadable. +This feature targets the Durable Task Scheduler; it does not reconfigure a Durable Functions host. + +## Payload contract + +| Option | Default | Behavior | +| -------------------------- | ---------------------- | ----------------------------------------------------------------------------------------- | +| `payloads.thresholdBytes` | 262144 (256 KiB) | Externalize at **greater than or equal** serialized UTF-8 bytes; maximum threshold 1 MiB. | +| `payloads.maxPayloadBytes` | 10485760 (10 MiB) | Reject larger externalized payloads before uploading. | +| `containerName` | `durabletask-payloads` | Lazily create a private container, once per successful initialization. | +| `compressionEnabled` | `true` | Gzip payload bytes; record `Content-Encoding: gzip`. | + +Sizes refer to serialized values, including JSON quotes/escapes, not JavaScript string length +or compressed size. Absent and empty payloads remain inline. With the feature disabled, +existing transport and payload behavior are unchanged. + +The SDK transparently externalizes client start/event/entity-signal inputs and management +reasons/outputs, activity results, orchestration actions and custom status, continue-as-new +inputs, rewritten history, and entity state/results/outgoing operations. State queries, +waits, history streams, and orchestration/activity/entity V1/V2 work items are hydrated before +conversion or user execution. Each message runs at most eight payload operations concurrently; +failure/cancellation stops further dispatch and drains already-started operations. + +New blobs have 32-character GUID names and `managed_by=dts` metadata on compressed and +uncompressed writes. Tokens are `blob:v2:{absoluteBlobUrl}`, without account keys or SAS. +Readers also recognize `blob:v1:{container}:{blobName}` in the configured container. +Same-service v2 reads preserve the configured credential/pipeline across containers, +including path-style/Azurite endpoints. Another account or endpoint requires an explicit +`TokenCredential`; account keys and configured SAS credentials are never forwarded there. + +Storage uses Azure SDK exponential retries: eight retries after the initial request, +250 ms initial delay, 10-second maximum delay, and two-minute request timeout. Optional +`clientOptions` accepts Azure SDK pipeline/retry overrides. Missing blobs and store/size +failures surface as `PayloadStorageException`. Permanent activity/orchestrator response +upload failures become non-retriable task/orchestration failures; transient failures remain +retryable. Entity upload failures and download failures propagate rather than fabricating success. + +Custom stores can extend `PayloadStore` from the core package. They must honor `AbortSignal`, +recognize only their own tokens, and return confirmed `PayloadDeleteOutcome` values for cleanup. +Deletion is optional. The provider's explicit deletion API has the safety contract below. + +## Explicit deletion safety + +Deletion checks the case-insensitive ownership marker, then uses that read's ETag and includes +snapshots. Unowned blobs are never deleted. Only confirmed `BlobNotFound`/`ContainerNotFound` +responses count as already absent; uncertain failures propagate. Configuring the provider alone +does not invoke deletion. Later cleanup phases add the authenticated backend lifecycle and runner. +`Deleted` means storage accepted deletion or confirmed a terminal outcome, **not** immediate +physical reclamation. Soft-delete retention and blob versions can continue to consume storage. + +## Local verification + +Unit tests use the real Azure SDK pipeline with an offline HTTP transport and checked-in +gzip vectors generated by .NET 10.0.13 using the pinned implementation's write/flush sequence. +Run `npm run test:unit -w @microsoft/durabletask-js-azureblob`. +The optional `test:azurite` suite requires a local-only +`DURABLETASK_BLOB_TEST_CONNECTION_STRING` and never provisions cloud resources. diff --git a/packages/durabletask-js-azureblob/jest.config.js b/packages/durabletask-js-azureblob/jest.config.js new file mode 100644 index 0000000..2696e51 --- /dev/null +++ b/packages/durabletask-js-azureblob/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/test"], + testMatch: ["**/*.spec.ts"], + moduleNameMapper: { + "^@microsoft/durabletask-js$": "/../durabletask-js/src/index.ts", + }, +}; diff --git a/packages/durabletask-js-azureblob/package.json b/packages/durabletask-js-azureblob/package.json new file mode 100644 index 0000000..1faf439 --- /dev/null +++ b/packages/durabletask-js-azureblob/package.json @@ -0,0 +1,44 @@ +{ + "name": "@microsoft/durabletask-js-azureblob", + "version": "0.4.0", + "description": "Azure Blob externalized payload storage for the Durable Task JavaScript SDK", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], + "scripts": { + "clean": "node -e \"require('fs').rmSync('dist', {recursive:true, force:true})\"", + "build": "npm run clean && tsc -p tsconfig.build.json", + "test": "jest --runInBand --detectOpenHandles", + "test:unit": "jest test --runInBand --detectOpenHandles", + "test:azurite": "jest test/azurite.spec.ts --runInBand --detectOpenHandles", + "prepublishOnly": "npm run build && npm run test" + }, + "engines": { + "node": ">=22.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/durabletask-js.git", + "directory": "packages/durabletask-js-azureblob" + }, + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.10.0", + "@azure/storage-blob": "^12.26.0" + }, + "peerDependencies": { + "@microsoft/durabletask-js": ">=0.4.1" + } +} diff --git a/packages/durabletask-js-azureblob/src/blob-payload-store.ts b/packages/durabletask-js-azureblob/src/blob-payload-store.ts new file mode 100644 index 0000000..c28037c --- /dev/null +++ b/packages/durabletask-js-azureblob/src/blob-payload-store.ts @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TokenCredential } from "@azure/core-auth"; +import { + BlobClient, + BlobServiceClient, + ContainerClient, + StoragePipelineOptions, + StorageRetryPolicyType, +} from "@azure/storage-blob"; +import { PayloadStore, PayloadStorageException, PayloadDeleteOutcome } from "@microsoft/durabletask-js"; +import { randomUUID } from "crypto"; +import { Writable } from "stream"; +import { pipeline } from "stream/promises"; +import { promisify } from "util"; +import { createGunzip, gzip } from "zlib"; + +const compress = promisify(gzip); + +export interface BlobPayloadStoreOptions { + connectionString?: string; + accountUri?: string | URL; + credential?: TokenCredential; + /** Defaults to durabletask-payloads. */ + containerName?: string; + /** Defaults to true. */ + compressionEnabled?: boolean; + /** Azure SDK HTTP, proxy, and retry configuration. */ + clientOptions?: StoragePipelineOptions; +} + +/** .NET-compatible blob:v2 writes and blob:v1/v2 reads, with ownership-checked conditional cleanup. */ +export class BlobPayloadStore extends PayloadStore { + private readonly _service: BlobServiceClient; + private readonly _container: ContainerClient; + private readonly _clientOptions: StoragePipelineOptions; + private _generation?: object; + private _initializing?: { promise: Promise; signal?: AbortSignal }; + + constructor(private readonly _options: BlobPayloadStoreOptions) { + super(); + const containerName = _options.containerName ?? "durabletask-payloads"; + if (!containerName) throw new TypeError("containerName must not be empty."); + this._clientOptions = { + ..._options.clientOptions, + retryOptions: { + retryPolicyType: StorageRetryPolicyType.EXPONENTIAL, + maxTries: 9, + retryDelayInMs: 250, + maxRetryDelayInMs: 10000, + tryTimeoutInMs: 120000, + ..._options.clientOptions?.retryOptions, + }, + }; + if (_options.accountUri && _options.credential) { + this._service = new BlobServiceClient(String(_options.accountUri), _options.credential, this._clientOptions); + } else if (_options.connectionString) { + this._service = BlobServiceClient.fromConnectionString(_options.connectionString, this._clientOptions); + } else { + throw new TypeError("Provide connectionString or both accountUri and credential."); + } + this._container = this._service.getContainerClient(containerName); + } + + isKnownPayloadToken(value: string): boolean { + return value.startsWith("blob:v1:") || value.startsWith("blob:v2:"); + } + + async upload(payload: string, signal?: AbortSignal): Promise { + signal?.throwIfAborted(); + const blob = this._container.getBlockBlobClient(randomUUID().replace(/-/g, "")); + const compressed = this._options.compressionEnabled ?? true; + const bytes = Buffer.from(payload, "utf8"); + const content = compressed ? await compress(bytes) : bytes; + signal?.throwIfAborted(); + for (let attempt = 0; ; attempt++) { + const generation = await this._ensureContainer(signal); + try { + await blob.upload(content, content.length, { + abortSignal: signal, + metadata: { managed_by: "dts" }, + blobHTTPHeaders: compressed ? { blobContentEncoding: "gzip" } : undefined, + }); + } catch (error) { + if (attempt === 0 && storageCode(error) === "ContainerNotFound") { + if (this._generation === generation) this._generation = undefined; + continue; + } + throw error; + } + const url = new URL(blob.url); + url.search = ""; + url.hash = ""; + url.username = ""; + url.password = ""; + return `blob:v2:${url.href}`; + } + } + + async download(token: string, signal?: AbortSignal): Promise { + signal?.throwIfAborted(); + const blob = this._blobClient(token); + try { + const response = await blob.download(0, undefined, { abortSignal: signal }); + const body = response.readableStreamBody; + if (!body) throw new PayloadStorageException("The payload blob response has no readable body."); + const chunks: Buffer[] = []; + const target = new Writable({ + write(chunk: Buffer, _encoding, callback) { + chunks.push(chunk); + callback(); + }, + }); + if (response.contentEncoding?.toLowerCase() === "gzip") { + await pipeline(body, createGunzip(), target, { signal }); + } else { + await pipeline(body, target, { signal }); + } + return Buffer.concat(chunks) + .toString("utf8") + .replace(/^\uFEFF/, ""); + } catch (error) { + if (storageStatus(error) === 404) { + throw new PayloadStorageException("The payload blob was not found. It may have been deleted.", { + cause: error, + }); + } + throw error; + } + } + + async delete(token: string, signal?: AbortSignal): Promise { + signal?.throwIfAborted(); + const blob = this._blobClient(token); + try { + const properties = await blob.getProperties({ abortSignal: signal }); + const owned = Object.entries(properties.metadata ?? {}).some( + ([key, value]) => key.toLowerCase() === "managed_by" && value === "dts", + ); + if (!owned) return PayloadDeleteOutcome.NotStoreOwned; + if (!properties.etag) throw new PayloadStorageException("The ownership response is missing its ETag."); + await blob.delete({ + abortSignal: signal, + deleteSnapshots: "include", + conditions: { ifMatch: properties.etag }, + }); + return PayloadDeleteOutcome.Deleted; + } catch (error) { + if ( + storageStatus(error) === 404 && + (storageCode(error) === "BlobNotFound" || storageCode(error) === "ContainerNotFound") + ) { + return PayloadDeleteOutcome.AlreadyAbsent; + } + throw error; + } + } + + private _blobClient(token: string): BlobClient { + if (token.startsWith("blob:v1:")) { + const rest = token.slice("blob:v1:".length); + const separator = rest.indexOf(":"); + if ( + separator < 1 || + separator === rest.length - 1 || + rest.slice(0, separator) !== this._container.containerName + ) { + throw new TypeError("Invalid external payload token or token container does not match."); + } + return this._container.getBlobClient(rest.slice(separator + 1)); + } + if (!token.startsWith("blob:v2:")) throw new TypeError("Invalid external payload token."); + let url: URL; + try { + url = new URL(token.slice("blob:v2:".length)); + } catch (error) { + throw new TypeError("Invalid external payload blob URL.", { cause: error }); + } + if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username || url.password) { + throw new TypeError("Invalid external payload blob URL."); + } + const decoded = new BlobClient(url.href); + if (!decoded.containerName || !decoded.name) throw new TypeError("Invalid external payload blob URL."); + const container = this._service.getContainerClient(decoded.containerName); + const configuredUrl = new URL(container.url); + // Match the service/container boundary, not normalized blob-name escaping. The SDK + // preserves path-style account prefixes and the configured credential/pipeline. + if ( + url.origin === configuredUrl.origin && + url.pathname.startsWith(configuredUrl.pathname.replace(/\/+$/, "") + "/") + ) { + return container.getBlobClient(decoded.name); + } + if (this._options.credential) return new BlobClient(url.href, this._options.credential, this._clientOptions); + throw new PayloadStorageException( + "Cross-account or cross-endpoint payload access requires identity authentication.", + ); + } + + private async _ensureContainer(signal?: AbortSignal): Promise { + signal?.throwIfAborted(); + if (this._generation) return this._generation; + if (!this._initializing) { + const initializing = { + signal, + promise: this._container.createIfNotExists({ abortSignal: signal }).then(() => { + this._generation = {}; + return this._generation; + }), + }; + this._initializing = initializing; + const clear = () => { + if (this._initializing === initializing) this._initializing = undefined; + }; + void initializing.promise.then(clear, clear); + } + const initializing = this._initializing; + try { + return await waitForInitialization(initializing.promise, signal); + } catch (error) { + if (initializing.signal?.aborted && !signal?.aborted) { + if (this._initializing === initializing) this._initializing = undefined; + return this._ensureContainer(signal); + } + throw error; + } + } +} + +async function waitForInitialization(promise: Promise, signal?: AbortSignal): Promise { + if (!signal) return promise; + signal.throwIfAborted(); + let onAbort = () => {}; + try { + return await new Promise((resolve, reject) => { + onAbort = () => reject(signal.reason); + signal.addEventListener("abort", onAbort, { once: true }); + promise.then(resolve, reject); + if (signal.aborted) onAbort(); + }); + } finally { + signal.removeEventListener("abort", onAbort); + } +} + +function storageCode(error: unknown): unknown { + if (error === null || typeof error !== "object") return undefined; + // HEAD failures have no response body; the Azure SDK exposes their service code in details. + if ( + "details" in error && + error.details !== null && + typeof error.details === "object" && + "errorCode" in error.details && + typeof error.details.errorCode === "string" + ) { + return error.details.errorCode; + } + return "code" in error ? error.code : undefined; +} + +function storageStatus(error: unknown): unknown { + return error !== null && typeof error === "object" && "statusCode" in error ? error.statusCode : undefined; +} diff --git a/packages/durabletask-js-azureblob/src/index.ts b/packages/durabletask-js-azureblob/src/index.ts new file mode 100644 index 0000000..1518fd4 --- /dev/null +++ b/packages/durabletask-js-azureblob/src/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export { BlobPayloadStore, BlobPayloadStoreOptions } from "./blob-payload-store"; diff --git a/packages/durabletask-js-azureblob/test/azurite.spec.ts b/packages/durabletask-js-azureblob/test/azurite.spec.ts new file mode 100644 index 0000000..54238a2 --- /dev/null +++ b/packages/durabletask-js-azureblob/test/azurite.spec.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { randomUUID, createHash } from "crypto"; +import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; +import { PayloadDeleteOutcome } from "@microsoft/durabletask-js"; +import { BlobPayloadStore } from "../src"; +import fixtures from "./dotnet-payload-fixtures.json"; + +const connectionString = process.env.DURABLETASK_BLOB_TEST_CONNECTION_STRING; +const localOnly = connectionString?.match(/BlobEndpoint=([^;]+)/i)?.[1]; +if (connectionString && (!localOnly || !["127.0.0.1", "localhost", "[::1]"].includes(new URL(localOnly).hostname))) { + throw new Error("Azurite tests require an explicit local-only BlobEndpoint."); +} + +(connectionString ? describe : describe.skip)("BlobPayloadStore with local Azurite", () => { + let service: BlobServiceClient; + let container: ContainerClient; + let containerName: string; + beforeEach(() => { + service = BlobServiceClient.fromConnectionString(connectionString!); + containerName = `payloads-${randomUUID()}`; + container = service.getContainerClient(containerName); + }); + afterEach(async () => { + await container.deleteIfExists(); + }); + + it.each([true, false])("round-trips UTF-8 payloads and same-account tokens (gzip=%s)", async (compressionEnabled) => { + const store = new BlobPayloadStore({ connectionString, containerName, compressionEnabled }); + const value = "\u00e9\ud83d\ude80".repeat(100000); + const token = await store.upload(value); + expect(await store.download(token)).toBe(value); + const url = new URL(token.slice("blob:v2:".length)); + const blobName = url.pathname.split("/").at(-1)!; + expect(await store.download(`blob:v1:${containerName}:${blobName}`)).toBe(value); + const properties = await container.getBlobClient(blobName).getProperties(); + expect(properties.metadata).toMatchObject({ managed_by: "dts" }); + expect(properties.contentEncoding ?? "").toBe(compressionEnabled ? "gzip" : ""); + const rotated = new BlobPayloadStore({ connectionString, containerName: `rotated-${randomUUID()}` }); + expect(await rotated.download(token)).toBe(value); + }); + + it.each(fixtures.vectors)("hydrates .NET-produced compressed blob $id", async (fixture) => { + await container.create(); + const blob = container.getBlockBlobClient("dotnet"); + const bytes = Buffer.from(fixture.gzipBase64, "base64"); + await blob.upload(bytes, bytes.length, { + metadata: { managed_by: "dts" }, + blobHTTPHeaders: { blobContentEncoding: "gzip" }, + }); + const store = new BlobPayloadStore({ connectionString, containerName }); + const actual = await store.download(`blob:v2:${blob.url}`); + expect(Buffer.byteLength(actual, "utf8")).toBe(fixture.utf8Bytes); + expect(createHash("sha256").update(actual, "utf8").digest("hex")).toBe(fixture.sha256); + }); + + it("deletes owned blobs and snapshots but never deletes unowned blobs", async () => { + const store = new BlobPayloadStore({ connectionString, containerName }); + const token = await store.upload("owned"); + const owned = container.getBlobClient(new URL(token.slice("blob:v2:".length)).pathname.split("/").at(-1)!); + await owned.createSnapshot(); + expect(await store.delete(token)).toBe(PayloadDeleteOutcome.Deleted); + expect(await store.delete(token)).toBe(PayloadDeleteOutcome.AlreadyAbsent); + expect(await owned.exists()).toBe(false); + const customer = container.getBlockBlobClient("customer"); + await customer.upload("customer data", 13); + expect(await store.delete(`blob:v2:${customer.url}`)).toBe(PayloadDeleteOutcome.NotStoreOwned); + expect(await customer.exists()).toBe(true); + }); +}); diff --git a/packages/durabletask-js-azureblob/test/blob-payload-store.spec.ts b/packages/durabletask-js-azureblob/test/blob-payload-store.spec.ts new file mode 100644 index 0000000..dd0548d --- /dev/null +++ b/packages/durabletask-js-azureblob/test/blob-payload-store.spec.ts @@ -0,0 +1,318 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Readable } from "stream"; +import { gunzipSync, gzipSync } from "zlib"; +import { IHttpClient, HttpOperationResponse, WebResource } from "@azure/storage-blob"; +import { PayloadDeleteOutcome, PayloadStorageException } from "@microsoft/durabletask-js"; +import { BlobPayloadStore } from "../src"; +import fixtures from "./dotnet-payload-fixtures.json"; + +const accountKey = Buffer.alloc(64).toString("base64"); +const connectionString = `DefaultEndpointsProtocol=https;AccountName=account;AccountKey=${accountKey};BlobEndpoint=https://account.invalid`; + +class RecordingClient implements IHttpClient { + requests: WebResource[] = []; + bodies: Buffer[] = []; + content = Buffer.from("payload"); + encoding = ""; + metadata: Record = { managed_by: "dts" }; + error: { status: number; code: string } | undefined; + failFirstUpload = false; + async sendRequest(request: WebResource): Promise { + this.requests.push(request); + const headers = request.headers.clone(); + for (const header of headers.headersArray()) headers.remove(header.name); + headers.set("x-ms-request-id", "offline"); + headers.set("etag", '"owned-etag"'); + headers.set("last-modified", new Date(0).toUTCString()); + headers.set("x-ms-blob-type", "BlockBlob"); + for (const [key, value] of Object.entries(this.metadata)) headers.set(`x-ms-meta-${key}`, value); + if (this.encoding) headers.set("content-encoding", this.encoding); + const container = new URL(request.url).searchParams.get("restype") === "container"; + const error = + this.failFirstUpload && request.method === "PUT" && !container + ? { status: 404, code: "ContainerNotFound" } + : this.error; + if (error) { + this.failFirstUpload = false; + headers.set("x-ms-error-code", error.code); + headers.set("content-type", "application/xml"); + return { + request, + status: error.status, + headers, + bodyAsText: + request.method === "HEAD" ? "" : `${error.code}offline`, + }; + } + if (request.method === "PUT" && !container) { + const body = typeof request.body === "function" ? request.body() : request.body; + if (!Buffer.isBuffer(body)) throw new Error("Expected a buffered upload"); + this.bodies.push(body); + } + headers.set("content-length", String(this.content.length)); + return { + request, + status: request.method === "PUT" ? 201 : request.method === "DELETE" ? 202 : 200, + headers, + readableStreamBody: Readable.from([this.content]), + bodyAsText: "", + }; + } +} + +describe("BlobPayloadStore", () => { + let http: RecordingClient; + beforeEach(() => { + http = new RecordingClient(); + }); + const make = (http: RecordingClient, options: Partial[0]> = {}) => + new BlobPayloadStore({ + connectionString, + clientOptions: { httpClient: http, retryOptions: { maxTries: 1 } }, + ...options, + }); + + it.each([true, false])( + "writes credential-free v2 GUID tokens and ownership metadata (gzip=%s)", + async (compressionEnabled) => { + const store = make(http, { compressionEnabled }); + const token = await store.upload("\u00e9\ud83d\ude80"); + expect(token).toMatch(/^blob:v2:https:\/\/account\.invalid\/durabletask-payloads\/[a-f0-9]{32}$/); + const upload = http.requests.find((request) => request.method === "PUT" && !request.url.includes("restype="))!; + expect(upload.headers.get("x-ms-meta-managed_by")).toBe("dts"); + expect(upload.headers.get("x-ms-blob-content-encoding") ?? "").toBe(compressionEnabled ? "gzip" : ""); + expect((compressionEnabled ? gunzipSync(http.bodies[0]) : http.bodies[0]).toString("utf8")).toBe( + "\u00e9\ud83d\ude80", + ); + expect(http.requests[0].headers.get("x-ms-blob-public-access")).toBeUndefined(); + }, + ); + + it("single-flights container initialization and recovers one deleted-container generation", async () => { + const store = make(http); + await Promise.all(Array.from({ length: 12 }, () => store.upload("data"))); + expect(http.requests.filter((r) => r.url.includes("restype=container"))).toHaveLength(1); + http.failFirstUpload = true; + await store.upload("recovered"); + expect(http.requests.filter((r) => r.url.includes("restype=container"))).toHaveLength(2); + expect(http.bodies).toHaveLength(13); + }); + + it("cancels a waiting initializer promptly without canceling or replacing the leader", async () => { + let release!: () => void; + let entered!: () => void; + const blocked = new Promise((resolve) => { + release = resolve; + }); + const started = new Promise((resolve) => { + entered = resolve; + }); + const send = http.sendRequest.bind(http); + http.sendRequest = async (request) => { + if (request.url.includes("restype=container")) { + entered(); + await blocked; + } + return send(request); + }; + const store = make(http, { compressionEnabled: false }); + const leader = new AbortController(); + const waiter = new AbortController(); + const removed = jest.spyOn(waiter.signal, "removeEventListener"); + const first = store.upload("first", leader.signal); + await started; + const reason = new Error("Only cancel the waiter"); + let observed: unknown; + const second = store.upload("second", waiter.signal).catch((error) => { + observed = error; + }); + try { + waiter.abort(reason); + await new Promise((resolve) => setImmediate(resolve)); + expect(observed).toBe(reason); + expect(leader.signal.aborted).toBe(false); + expect(removed).toHaveBeenCalledWith("abort", expect.any(Function)); + release(); + await first; + await store.upload("third"); + expect(http.requests.filter((r) => r.url.includes("restype=container"))).toHaveLength(1); + expect(http.bodies.map((body) => body.toString())).toEqual(["first", "third"]); + } finally { + release(); + await Promise.allSettled([first, second]); + } + }); + + it("lets a surviving waiter initialize after the previous owner is canceled", async () => { + let entered!: () => void; + const started = new Promise((resolve) => { + entered = resolve; + }); + const send = http.sendRequest.bind(http); + let firstContainerRequest = true; + http.sendRequest = async (request) => { + if (request.url.includes("restype=container") && firstContainerRequest) { + firstContainerRequest = false; + if (!request.abortSignal) throw new Error("The owner must propagate cancellation."); + const signal = request.abortSignal; + await new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => + reject(Object.assign(new Error("Owner canceled"), { name: "AbortError" })), + ); + entered(); + }); + } + return send(request); + }; + const owner = new AbortController(); + const store = make(http, { compressionEnabled: false }); + const first = store.upload("owner", owner.signal); + const failedOwner = expect(first).rejects.toThrow(); + await started; + const survivor = store.upload("survivor"); + owner.abort(); + await failedOwner; + await expect(survivor).resolves.toMatch(/^blob:v2:/); + expect(http.bodies.map((body) => body.toString())).toEqual(["survivor"]); + }); + + it.each(["gzip", "GZip", ""])("reads UTF-8 storage representation with encoding %s", async (encoding) => { + http.content = encoding ? gzipSync(Buffer.from("\u00e9\ud83d\ude80")) : Buffer.from("\u00e9\ud83d\ude80"); + http.encoding = encoding; + expect(await make(http).download("blob:v1:durabletask-payloads:folder/a b%2Fc")).toBe("\u00e9\ud83d\ude80"); + expect(new URL(http.requests[0].url).pathname).toBe("/durabletask-payloads/folder/a%20b%252Fc"); + }); + + it.each(fixtures.vectors)("reads actual .NET 10 GZipStream fixture $id", async (fixture) => { + http.encoding = "gzip"; + http.content = Buffer.from(fixture.gzipBase64, "base64"); + const expected = fixture.seed.repeat(fixture.repeat) + fixture.suffix; + const actual = await make(http).download("blob:v2:https://account.invalid/durabletask-payloads/dotnet"); + expect(actual).toBe(expected); + expect(Buffer.byteLength(actual, "utf8")).toBe(fixture.utf8Bytes); + }); + + it.each([ + ["https://account.invalid", "https://account.invalid/old/folder/a%20b%252Fc"], + ["http://127.0.0.1:10000/accounta", "http://127.0.0.1:10000/accounta/old/blob"], + ])("preserves configured SharedKey pipeline across containers in %s", async (endpoint, target) => { + const store = make(http, { + connectionString: `DefaultEndpointsProtocol=http;AccountName=account;AccountKey=${accountKey};BlobEndpoint=${endpoint}`, + }); + expect(await store.download(`blob:v2:${target}`)).toBe("payload"); + expect(await store.delete(`blob:v2:${target}`)).toBe(PayloadDeleteOutcome.Deleted); + expect(http.requests.map((r) => r.method)).toEqual(["GET", "HEAD", "DELETE"]); + for (const request of http.requests) { + const url = new URL(request.url); + expect(url.origin + url.pathname).toBe(target); + expect(url.searchParams.get("timeout")).toBe("120"); + expect(request.headers.get("authorization")).toMatch(/^SharedKey /); + } + expect(http.requests[2].headers.get("if-match")).toBe('"owned-etag"'); + expect(http.requests[2].headers.get("x-ms-delete-snapshots")).toBe("include"); + }); + + it.each([ + "https://other.invalid/old/blob", + "http://account.invalid/old/blob", + "https://account.invalid:444/old/blob", + "http://127.0.0.1:10000/accountb/old/blob", + ])("never forwards configured account keys to %s", async (target) => { + const store = make(http); + await expect(store.download(`blob:v2:${target}`)).rejects.toBeInstanceOf(PayloadStorageException); + await expect(store.delete(`blob:v2:${target}`)).rejects.toBeInstanceOf(PayloadStorageException); + expect(http.requests).toHaveLength(0); + }); + + it("uses identity, not the configured account key, for cross-account reads", async () => { + const credential = { + getToken: async () => ({ token: "synthetic-access-token", expiresOnTimestamp: Date.now() + 3600000 }), + }; + const store = make(http, { credential }); + expect(await store.download("blob:v2:https://other.invalid/old/blob")).toBe("payload"); + expect(http.requests[0].headers.get("authorization")).toBe("Bearer synthetic-access-token"); + }); + + it("supports account URI and identity without a connection string", async () => { + const store = make(http, { + connectionString: undefined, + accountUri: "https://account.invalid", + credential: { + getToken: async () => ({ token: "synthetic-access-token", expiresOnTimestamp: Date.now() + 3600000 }), + }, + }); + expect(await store.download("blob:v2:https://account.invalid/durabletask-payloads/blob")).toBe("payload"); + expect(http.requests[0].headers.get("authorization")).toBe("Bearer synthetic-access-token"); + }); + + it("classifies the service endpoint independently of normalized blob-name escaping", async () => { + expect(await make(http).download("blob:v2:https://account.invalid/old/folder%2fblob")).toBe("payload"); + expect(http.requests[0].headers.get("authorization")).toMatch(/^SharedKey /); + expect(new URL(http.requests[0].url).pathname).toBe("/old/folder/blob"); + }); + + it("rejects another path-style account on the same host without sending SharedKey", async () => { + const store = make(http, { + connectionString: `DefaultEndpointsProtocol=http;AccountName=accounta;AccountKey=${accountKey};BlobEndpoint=http://127.0.0.1:10000/accounta`, + }); + await expect(store.download("blob:v2:http://127.0.0.1:10000/accountb/old/blob")).rejects.toBeInstanceOf( + PayloadStorageException, + ); + expect(http.requests).toHaveLength(0); + }); + + it("preserves a configured SAS across same-account containers without persisting it", async () => { + const store = make(http, { + connectionString: + "AccountName=account;SharedAccessSignature=sv=2025-01-05&sr=c&sp=rwd&sig=synthetic;BlobEndpoint=https://account.invalid", + }); + const token = await store.upload("payload"); + expect(token).not.toContain("sig="); + await store.download("blob:v2:https://account.invalid/old/blob"); + expect(new URL(http.requests.at(-1)!.url).searchParams.get("sig")).toBe("synthetic"); + }); + + it.each([ + [{}, PayloadDeleteOutcome.NotStoreOwned], + [{ managed_by: "customer" }, PayloadDeleteOutcome.NotStoreOwned], + [{ MANAGED_BY: "dts" }, PayloadDeleteOutcome.Deleted], + ])("checks ownership case-insensitively before conditional deletion (%#)", async (metadata, outcome) => { + http.metadata = metadata; + expect(await make(http).delete("blob:v2:https://account.invalid/durabletask-payloads/blob")).toBe(outcome); + expect(http.requests.some((r) => r.method === "DELETE")).toBe(outcome === PayloadDeleteOutcome.Deleted); + }); + + it.each(["BlobNotFound", "ContainerNotFound"])("confirms absence only for %s", async (code) => { + http.error = { status: 404, code }; + expect(await make(http).delete("blob:v2:https://account.invalid/durabletask-payloads/blob")).toBe( + PayloadDeleteOutcome.AlreadyAbsent, + ); + await expect( + make(http).download("blob:v2:https://account.invalid/durabletask-payloads/blob"), + ).rejects.toBeInstanceOf(PayloadStorageException); + }); + + it.each([ + [404, ""], + [412, "ConditionNotMet"], + [403, "AuthorizationPermissionMismatch"], + ])("propagates uncertain storage failures %s/%s", async (status, code) => { + http.error = { status, code }; + await expect(make(http).delete("blob:v2:https://account.invalid/durabletask-payloads/blob")).rejects.toMatchObject({ + statusCode: status, + }); + }); + + it.each([ + "blob:v1:bad", + "blob:v1:other:blob", + "blob:v2:invalid", + "blob:v2:https://account.invalid/container/", + "blob:v3:future", + ])("rejects malformed or mismatched tokens without storage I/O (%s)", async (token) => { + await expect(make(http).download(token)).rejects.toBeInstanceOf(TypeError); + expect(http.requests).toHaveLength(0); + }); +}); diff --git a/packages/durabletask-js-azureblob/test/dotnet-payload-fixtures.json b/packages/durabletask-js-azureblob/test/dotnet-payload-fixtures.json new file mode 100644 index 0000000..514e22a --- /dev/null +++ b/packages/durabletask-js-azureblob/test/dotnet-payload-fixtures.json @@ -0,0 +1,91 @@ +{ + "referenceCommit": "91739687ff647699079cdadd627daa00562982b7", + "runtime": ".NET 10.0.13", + "compression": "System.IO.Compression.GZipStream / CompressionLevel.Optimal", + "vectors": [ + { + "id": "ascii-below-threshold", + "seed": "a", + "repeat": 262143, + "suffix": "", + "utf8Bytes": 262143, + "utf16CodeUnits": 262143, + "sha256": "0379854ae435b4c82999632f674694ce1f98103f645f44ba68c901a83a2b00fd", + "gzipBase64": "H4sIAAAAAAAACuzBMQEAAADCoKzrX8LbDkABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJwBAAD//wMAs7HRUv//AwA=", + "requiresExternalization": false, + "exceedsMaximum": false + }, + { + "id": "ascii-at-threshold", + "seed": "a", + "repeat": 262144, + "suffix": "", + "utf8Bytes": 262144, + "utf16CodeUnits": 262144, + "sha256": "dd3dde87623d9a6b354c68c943d189c89c63652d945e7bbdf0986cae91a49521", + "gzipBase64": "H4sIAAAAAAAACuzBMQEAAADCoKzrX8LbDkABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALwBAAD//wMAxI2NugAABAA=", + "requiresExternalization": true, + "exceedsMaximum": false + }, + { + "id": "ascii-above-threshold", + "seed": "a", + "repeat": 262145, + "suffix": "", + "utf8Bytes": 262145, + "utf16CodeUnits": 262145, + "sha256": "c592f4a6b099700b5050ce8bc67367f0c8f44810203124e86405c3e7b6f1a2ba", + "gzipBase64": "H4sIAAAAAAAACuzBMQEAAADCoKzrX8LfDEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJwBAAD//wMAZzUEdAEABAA=", + "requiresExternalization": true, + "exceedsMaximum": false + }, + { + "id": "multibyte-at-threshold", + "seed": "é", + "repeat": 131072, + "suffix": "", + "utf8Bytes": 262144, + "utf16CodeUnits": 131072, + "sha256": "94914398e4fe14ac182b9e6080caa078bbde122682c352744929d55f7d038d10", + "gzipBase64": "H4sIAAAAAAAACuzQwRAAAAACMH+UOBLrncNuBmuKAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOAGBgAA//8DAONphUEAAAQA", + "requiresExternalization": true, + "exceedsMaximum": false + }, + { + "id": "supplementary-at-threshold", + "seed": "🚀", + "repeat": 65536, + "suffix": "", + "utf8Bytes": 262144, + "utf16CodeUnits": 131072, + "sha256": "f802f2cdafbd3f25ef8ce8e48994ef484a1b860addef41067f94e621945eac6e", + "gzipBase64": "H4sIAAAAAAAACuzSMREAAAgDMevoYMFiJXRFRIY38JfcTuQBAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDDAAAMMMMAAAwwwwAADDDwDBQAA//8DAIXOWJsAAAQA", + "requiresExternalization": true, + "exceedsMaximum": false + }, + { + "id": "ascii-at-maximum", + "seed": "a", + "repeat": 10485760, + "suffix": "", + "utf8Bytes": 10485760, + "utf16CodeUnits": 10485760, + "sha256": "b5eec3f68ef64d15e82dad91ff908582c5f081e61a62e22427af9bec2cd35f8d", + "gzipBase64": "H4sIAAAAAAAACuzBgQAAAACAINb9JRapCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgNmDAwEAAAAAIP/XRlBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVhD44JAAAAEAZltX+JxfABBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3AQAA//8DALIc12sAAKAA", + "requiresExternalization": true, + "exceedsMaximum": false + }, + { + "id": "ascii-above-maximum", + "seed": "a", + "repeat": 10485761, + "suffix": "", + "utf8Bytes": 10485761, + "utf16CodeUnits": 10485761, + "sha256": "4ea73dbccbce283083f78555e86595e0b345c46ff188509412fee1c68914d0cb", + "gzipBase64": "H4sIAAAAAAAACuzBgQAAAACAINb9JRapCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgNmDAwEAAAAAIP/XRlBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVhD44JAAAAEAZltX+JxfABBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AQAA//8DAP+7s80BAKAA", + "requiresExternalization": true, + "exceedsMaximum": true + } + ] +} diff --git a/packages/durabletask-js-azureblob/tsconfig.build.json b/packages/durabletask-js-azureblob/tsconfig.build.json new file mode 100644 index 0000000..b4fc9d0 --- /dev/null +++ b/packages/durabletask-js-azureblob/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "baseUrl": ".", + "paths": { "@microsoft/durabletask-js": ["../durabletask-js/dist/index"] } + } +} diff --git a/packages/durabletask-js-azureblob/tsconfig.json b/packages/durabletask-js-azureblob/tsconfig.json new file mode 100644 index 0000000..8acab8b --- /dev/null +++ b/packages/durabletask-js-azureblob/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 1dfb565..20ccaaa 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,6 +14,7 @@ "paths": { "@microsoft/durabletask-js": ["packages/durabletask-js/src/index.ts"], "@microsoft/durabletask-js-azuremanaged": ["packages/durabletask-js-azuremanaged/src/index.ts"], + "@microsoft/durabletask-js-azureblob": ["packages/durabletask-js-azureblob/src/index.ts"], "@microsoft/durabletask-js-export-history": ["packages/durabletask-js-export-history/src/index.ts"] } }