// src/headers.ts var NF_ERROR = "x-nf-error"; var NF_REQUEST_ID = "x-nf-request-id"; // src/util.ts var BlobsInternalError = class extends Error { constructor(res) { let details = res.headers.get(NF_ERROR) || `${res.status} status code`; if (res.headers.has(NF_REQUEST_ID)) { details += `, ID: ${res.headers.get(NF_REQUEST_ID)}`; } super(`Netlify Blobs has generated an internal error (${details})`); this.name = "BlobsInternalError"; } }; var collectIterator = async (iterator) => { const result = []; for await (const item of iterator) { result.push(item); } return result; }; var isNodeError = (error) => error instanceof Error; var base64Decode = (input) => { const { Buffer } = globalThis; if (Buffer) { return Buffer.from(input, "base64").toString(); } return atob(input); }; var base64Encode = (input) => { const { Buffer } = globalThis; if (Buffer) { return Buffer.from(input).toString("base64"); } return btoa(input); }; // src/environment.ts var getEnvironment = () => { const { Deno, Netlify, process } = globalThis; return Netlify?.env ?? Deno?.env ?? { delete: (key) => delete process?.env[key], get: (key) => process?.env[key], has: (key) => Boolean(process?.env[key]), set: (key, value) => { if (process?.env) { process.env[key] = value; } }, toObject: () => process?.env ?? {} }; }; var getEnvironmentContext = () => { const context = globalThis.netlifyBlobsContext || getEnvironment().get("NETLIFY_BLOBS_CONTEXT"); if (typeof context !== "string" || !context) { return {}; } const data = base64Decode(context); try { return JSON.parse(data); } catch { } return {}; }; var setEnvironmentContext = (context) => { const encodedContext = base64Encode(JSON.stringify(context)); getEnvironment().set("NETLIFY_BLOBS_CONTEXT", encodedContext); }; var MissingBlobsEnvironmentError = class extends Error { constructor(requiredProperties) { super( `The environment has not been configured to use Netlify Blobs. To use it manually, supply the following properties when creating a store: ${requiredProperties.join( ", " )}` ); this.name = "MissingBlobsEnvironmentError"; } }; // src/metadata.ts var BASE64_PREFIX = "b64;"; var METADATA_HEADER_INTERNAL = "x-amz-meta-user"; var METADATA_HEADER_EXTERNAL = "netlify-blobs-metadata"; var METADATA_MAX_SIZE = 2 * 1024; var encodeMetadata = (metadata) => { if (!metadata) { return null; } const encodedObject = base64Encode(JSON.stringify(metadata)); const payload = `b64;${encodedObject}`; if (METADATA_HEADER_EXTERNAL.length + payload.length > METADATA_MAX_SIZE) { throw new Error("Metadata object exceeds the maximum size"); } return payload; }; var decodeMetadata = (header) => { if (!header || !header.startsWith(BASE64_PREFIX)) { return {}; } const encodedData = header.slice(BASE64_PREFIX.length); const decodedData = base64Decode(encodedData); const metadata = JSON.parse(decodedData); return metadata; }; var getMetadataFromResponse = (response) => { if (!response.headers) { return {}; } const value = response.headers.get(METADATA_HEADER_EXTERNAL) || response.headers.get(METADATA_HEADER_INTERNAL); try { return decodeMetadata(value); } catch { throw new Error( "An internal error occurred while trying to retrieve the metadata for an entry. Please try updating to the latest version of the Netlify Blobs client." ); } }; // src/consistency.ts var BlobsConsistencyError = class extends Error { constructor() { super( `Netlify Blobs has failed to perform a read using strong consistency because the environment has not been configured with a 'uncachedEdgeURL' property` ); this.name = "BlobsConsistencyError"; } }; // src/region.ts var REGION_AUTO = "auto"; var regions = { "us-east-1": true, "us-east-2": true, "eu-central-1": true, "ap-southeast-1": true, "ap-southeast-2": true }; var isValidRegion = (input) => Object.keys(regions).includes(input); var InvalidBlobsRegionError = class extends Error { constructor(region) { super( `${region} is not a supported Netlify Blobs region. Supported values are: ${Object.keys(regions).join(", ")}.` ); this.name = "InvalidBlobsRegionError"; } }; // src/retry.ts var DEFAULT_RETRY_DELAY = getEnvironment().get("NODE_ENV") === "test" ? 1 : 5e3; var MIN_RETRY_DELAY = 1e3; var MAX_RETRY = 5; var RATE_LIMIT_HEADER = "X-RateLimit-Reset"; var fetchAndRetry = async (fetch, url, options, attemptsLeft = MAX_RETRY) => { try { const res = await fetch(url, options); if (attemptsLeft > 0 && (res.status === 429 || res.status >= 500)) { const delay = getDelay(res.headers.get(RATE_LIMIT_HEADER)); await sleep(delay); return fetchAndRetry(fetch, url, options, attemptsLeft - 1); } return res; } catch (error) { if (attemptsLeft === 0) { throw error; } const delay = getDelay(); await sleep(delay); return fetchAndRetry(fetch, url, options, attemptsLeft - 1); } }; var getDelay = (rateLimitReset) => { if (!rateLimitReset) { return DEFAULT_RETRY_DELAY; } return Math.max(Number(rateLimitReset) * 1e3 - Date.now(), MIN_RETRY_DELAY); }; var sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); // src/client.ts var SIGNED_URL_ACCEPT_HEADER = "application/json;type=signed-url"; var Client = class { constructor({ apiURL, consistency, edgeURL, fetch, region, siteID, token, uncachedEdgeURL }) { this.apiURL = apiURL; this.consistency = consistency ?? "eventual"; this.edgeURL = edgeURL; this.fetch = fetch ?? globalThis.fetch; this.region = region; this.siteID = siteID; this.token = token; this.uncachedEdgeURL = uncachedEdgeURL; if (!this.fetch) { throw new Error( "Netlify Blobs could not find a `fetch` client in the global scope. You can either update your runtime to a version that includes `fetch` (like Node.js 18.0.0 or above), or you can supply your own implementation using the `fetch` property." ); } } async getFinalRequest({ consistency: opConsistency, key, metadata, method, parameters = {}, storeName }) { const encodedMetadata = encodeMetadata(metadata); const consistency = opConsistency ?? this.consistency; let urlPath = `/${this.siteID}`; if (storeName) { urlPath += `/${storeName}`; } if (key) { urlPath += `/${key}`; } if (this.edgeURL) { if (consistency === "strong" && !this.uncachedEdgeURL) { throw new BlobsConsistencyError(); } const headers = { authorization: `Bearer ${this.token}` }; if (encodedMetadata) { headers[METADATA_HEADER_INTERNAL] = encodedMetadata; } if (this.region) { urlPath = `/region:${this.region}${urlPath}`; } const url2 = new URL(urlPath, consistency === "strong" ? this.uncachedEdgeURL : this.edgeURL); for (const key2 in parameters) { url2.searchParams.set(key2, parameters[key2]); } return { headers, url: url2.toString() }; } const apiHeaders = { authorization: `Bearer ${this.token}` }; const url = new URL(`/api/v1/blobs${urlPath}`, this.apiURL ?? "https://api.netlify.com"); for (const key2 in parameters) { url.searchParams.set(key2, parameters[key2]); } if (this.region) { url.searchParams.set("region", this.region); } if (storeName === void 0 || key === void 0) { return { headers: apiHeaders, url: url.toString() }; } if (encodedMetadata) { apiHeaders[METADATA_HEADER_EXTERNAL] = encodedMetadata; } if (method === "head" /* HEAD */ || method === "delete" /* DELETE */) { return { headers: apiHeaders, url: url.toString() }; } const res = await this.fetch(url.toString(), { headers: { ...apiHeaders, accept: SIGNED_URL_ACCEPT_HEADER }, method }); if (res.status !== 200) { throw new BlobsInternalError(res); } const { url: signedURL } = await res.json(); const userHeaders = encodedMetadata ? { [METADATA_HEADER_INTERNAL]: encodedMetadata } : void 0; return { headers: userHeaders, url: signedURL }; } async makeRequest({ body, consistency, headers: extraHeaders, key, metadata, method, parameters, storeName }) { const { headers: baseHeaders = {}, url } = await this.getFinalRequest({ consistency, key, metadata, method, parameters, storeName }); const headers = { ...baseHeaders, ...extraHeaders }; if (method === "put" /* PUT */) { headers["cache-control"] = "max-age=0, stale-while-revalidate=60"; } const options = { body, headers, method }; if (body instanceof ReadableStream) { options.duplex = "half"; } return fetchAndRetry(this.fetch, url, options); } }; var getClientOptions = (options, contextOverride) => { const context = contextOverride ?? getEnvironmentContext(); const siteID = context.siteID ?? options.siteID; const token = context.token ?? options.token; if (!siteID || !token) { throw new MissingBlobsEnvironmentError(["siteID", "token"]); } if (options.region !== void 0 && !isValidRegion(options.region)) { throw new InvalidBlobsRegionError(options.region); } const clientOptions = { apiURL: context.apiURL ?? options.apiURL, consistency: options.consistency, edgeURL: context.edgeURL ?? options.edgeURL, fetch: options.fetch, region: options.region, siteID, token, uncachedEdgeURL: context.uncachedEdgeURL ?? options.uncachedEdgeURL }; return clientOptions; }; export { BlobsInternalError, collectIterator, isNodeError, base64Decode, getEnvironmentContext, setEnvironmentContext, MissingBlobsEnvironmentError, METADATA_HEADER_INTERNAL, encodeMetadata, decodeMetadata, getMetadataFromResponse, REGION_AUTO, SIGNED_URL_ACCEPT_HEADER, Client, getClientOptions };