"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/server.ts var server_exports = {}; __export(server_exports, { BlobsServer: () => BlobsServer, Operation: () => Operation }); module.exports = __toCommonJS(server_exports); var import_node_crypto = require("crypto"); var import_node_fs = require("fs"); var import_node_http = __toESM(require("http"), 1); var import_node_os = require("os"); var import_node_path = require("path"); var import_node_process = require("process"); var import_node_stream = __toESM(require("stream"), 1); var import_node_util = require("util"); // src/util.ts var isNodeError = (error) => error instanceof Error; var base64Decode = (input) => { const { Buffer: Buffer2 } = globalThis; if (Buffer2) { return Buffer2.from(input, "base64").toString(); } return atob(input); }; var base64Encode = (input) => { const { Buffer: Buffer2 } = globalThis; if (Buffer2) { return Buffer2.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 ?? {} }; }; // 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; }; // src/retry.ts var DEFAULT_RETRY_DELAY = getEnvironment().get("NODE_ENV") === "test" ? 1 : 5e3; // src/client.ts var SIGNED_URL_ACCEPT_HEADER = "application/json;type=signed-url"; // src/server.ts var API_URL_PATH = /\/api\/v1\/blobs\/(?[^/]+)\/(?[^/]+)\/?(?[^?]*)/; var LEGACY_API_URL_PATH = /\/api\/v1\/sites\/(?[^/]+)\/blobs\/?(?[^?]*)/; var LEGACY_DEFAULT_STORE = "production"; var REGION_PREFIX = "region:"; var Operation = /* @__PURE__ */ ((Operation2) => { Operation2["DELETE"] = "delete"; Operation2["GET"] = "get"; Operation2["GET_METADATA"] = "getMetadata"; Operation2["LIST"] = "list"; Operation2["SET"] = "set"; return Operation2; })(Operation || {}); var pipeline = (0, import_node_util.promisify)(import_node_stream.default.pipeline); var BlobsServer = class _BlobsServer { constructor({ debug, directory, logger, onRequest, port, token }) { this.address = ""; this.debug = debug === true; this.directory = directory; this.logger = logger ?? console.log; this.onRequest = onRequest; this.port = port || 0; this.token = token; this.tokenHash = (0, import_node_crypto.createHmac)("sha256", Math.random.toString()).update(token ?? Math.random.toString()).digest("hex"); } dispatchOnRequestEvent(type, url) { if (!this.onRequest) { return; } const urlPath = url instanceof URL ? url.pathname + url.search : url; this.onRequest({ type, url: urlPath }); } logDebug(...message) { if (!this.debug) { return; } this.logger("[Netlify Blobs server]", ...message); } async delete(req, res) { const apiMatch = this.parseAPIRequest(req); if (apiMatch?.useSignedURL) { return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })); } const url = new URL(apiMatch?.url ?? req.url ?? "", this.address); const { dataPath, key, metadataPath } = this.getLocalPaths(url); if (!dataPath || !key) { return this.sendResponse(req, res, 400); } try { await import_node_fs.promises.rm(metadataPath, { force: true, recursive: true }); } catch { } try { await import_node_fs.promises.rm(dataPath, { force: true, recursive: true }); } catch (error) { if (!isNodeError(error) || error.code !== "ENOENT") { return this.sendResponse(req, res, 500); } } return this.sendResponse(req, res, 204); } async get(req, res) { const apiMatch = this.parseAPIRequest(req); const url = apiMatch?.url ?? new URL(req.url ?? "", this.address); if (apiMatch?.key && apiMatch?.useSignedURL) { return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })); } const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(apiMatch?.url ?? url); if (!rootPath) { return this.sendResponse(req, res, 400); } if (!dataPath || !metadataPath) { return this.listStores(req, res, rootPath, url.searchParams.get("prefix") ?? ""); } if (!key) { return this.listBlobs({ dataPath, metadataPath, rootPath, req, res, url }); } this.dispatchOnRequestEvent("get" /* GET */, url); const headers = {}; try { const rawData = await import_node_fs.promises.readFile(metadataPath, "utf8"); const metadata = JSON.parse(rawData); const encodedMetadata = encodeMetadata(metadata); if (encodedMetadata) { headers[METADATA_HEADER_INTERNAL] = encodedMetadata; } } catch (error) { if (!isNodeError(error) || error.code !== "ENOENT") { this.logDebug("Could not read metadata file:", error); } } for (const name in headers) { res.setHeader(name, headers[name]); } const stream2 = (0, import_node_fs.createReadStream)(dataPath); stream2.on("error", (error) => { if (error.code === "EISDIR" || error.code === "ENOENT") { return this.sendResponse(req, res, 404); } return this.sendResponse(req, res, 500); }); stream2.pipe(res); } async head(req, res) { const url = this.parseAPIRequest(req)?.url ?? new URL(req.url ?? "", this.address); const { dataPath, key, metadataPath } = this.getLocalPaths(url); if (!dataPath || !metadataPath || !key) { return this.sendResponse(req, res, 400); } try { const rawData = await import_node_fs.promises.readFile(metadataPath, "utf8"); const metadata = JSON.parse(rawData); const encodedMetadata = encodeMetadata(metadata); if (encodedMetadata) { res.setHeader(METADATA_HEADER_INTERNAL, encodedMetadata); } } catch (error) { if (isNodeError(error) && (error.code === "ENOENT" || error.code === "ISDIR")) { return this.sendResponse(req, res, 404); } this.logDebug("Could not read metadata file:", error); return this.sendResponse(req, res, 500); } res.end(); } async listBlobs(options) { const { dataPath, rootPath, req, res, url } = options; const directories = url.searchParams.get("directories") === "true"; const prefix = url.searchParams.get("prefix") ?? ""; const result = { blobs: [], directories: [] }; this.dispatchOnRequestEvent("list" /* LIST */, url); try { await _BlobsServer.walk({ directories, path: dataPath, prefix, rootPath, result }); } catch (error) { if (!isNodeError(error) || error.code !== "ENOENT") { this.logDebug("Could not perform list:", error); return this.sendResponse(req, res, 500); } } res.setHeader("content-type", "application/json"); return this.sendResponse(req, res, 200, JSON.stringify(result)); } async listStores(req, res, rootPath, prefix) { try { const allStores = await import_node_fs.promises.readdir(rootPath); const filteredStores = allStores.map((store) => import_node_process.platform === "win32" ? decodeURIComponent(store) : store).filter((store) => store.startsWith(prefix)); return this.sendResponse(req, res, 200, JSON.stringify({ stores: filteredStores })); } catch (error) { this.logDebug("Could not list stores:", error); return this.sendResponse(req, res, 500); } } async put(req, res) { const apiMatch = this.parseAPIRequest(req); if (apiMatch) { return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })); } const url = new URL(req.url ?? "", this.address); const { dataPath, key, metadataPath } = this.getLocalPaths(url); if (!dataPath || !key || !metadataPath) { return this.sendResponse(req, res, 400); } const metadataHeader = req.headers[METADATA_HEADER_INTERNAL]; const metadata = decodeMetadata(Array.isArray(metadataHeader) ? metadataHeader[0] : metadataHeader ?? null); try { const tempDirectory = await import_node_fs.promises.mkdtemp((0, import_node_path.join)((0, import_node_os.tmpdir)(), "netlify-blobs")); const relativeDataPath = (0, import_node_path.relative)(this.directory, dataPath); const tempDataPath = (0, import_node_path.join)(tempDirectory, relativeDataPath); await import_node_fs.promises.mkdir((0, import_node_path.dirname)(tempDataPath), { recursive: true }); await pipeline(req, (0, import_node_fs.createWriteStream)(tempDataPath)); await import_node_fs.promises.mkdir((0, import_node_path.dirname)(dataPath), { recursive: true }); await import_node_fs.promises.copyFile(tempDataPath, dataPath); await import_node_fs.promises.rm(tempDirectory, { force: true, recursive: true }); await import_node_fs.promises.mkdir((0, import_node_path.dirname)(metadataPath), { recursive: true }); await import_node_fs.promises.writeFile(metadataPath, JSON.stringify(metadata)); } catch (error) { this.logDebug("Error when writing data:", error); return this.sendResponse(req, res, 500); } return this.sendResponse(req, res, 200); } /** * Parses the URL and returns the filesystem paths where entries and metadata * should be stored. */ getLocalPaths(url) { if (!url) { return {}; } let parts = url.pathname.split("/").slice(1); if (parts[0].startsWith(REGION_PREFIX)) { parts = parts.slice(1); } const [siteID, rawStoreName, ...key] = parts; if (!siteID) { return {}; } const rootPath = (0, import_node_path.resolve)(this.directory, "entries", siteID); if (!rawStoreName) { return { rootPath }; } const storeName = import_node_process.platform === "win32" ? encodeURIComponent(rawStoreName) : rawStoreName; const storePath = (0, import_node_path.resolve)(rootPath, storeName); const dataPath = (0, import_node_path.resolve)(storePath, ...key); const metadataPath = (0, import_node_path.resolve)(this.directory, "metadata", siteID, storeName, ...key); return { dataPath, key: key.join("/"), metadataPath, rootPath: storePath }; } handleRequest(req, res) { if (!req.url || !this.validateAccess(req)) { return this.sendResponse(req, res, 403); } switch (req.method?.toLowerCase()) { case "delete" /* DELETE */: { this.dispatchOnRequestEvent("delete" /* DELETE */, req.url); return this.delete(req, res); } case "get" /* GET */: { return this.get(req, res); } case "put" /* PUT */: { this.dispatchOnRequestEvent("set" /* SET */, req.url); return this.put(req, res); } case "head" /* HEAD */: { this.dispatchOnRequestEvent("getMetadata" /* GET_METADATA */, req.url); return this.head(req, res); } default: return this.sendResponse(req, res, 405); } } /** * Tries to parse a URL as being an API request and returns the different * components, such as the store name, site ID, key, and signed URL. */ parseAPIRequest(req) { if (!req.url) { return null; } const apiURLMatch = req.url.match(API_URL_PATH); if (apiURLMatch) { const key = apiURLMatch.groups?.key; const siteID = apiURLMatch.groups?.site_id; const storeName = apiURLMatch.groups?.store_name; const urlPath = [siteID, storeName, key].filter(Boolean); const url = new URL(`/${urlPath.join("/")}?signature=${this.tokenHash}`, this.address); return { key, siteID, storeName, url, useSignedURL: req.headers.accept === SIGNED_URL_ACCEPT_HEADER }; } const legacyAPIURLMatch = req.url.match(LEGACY_API_URL_PATH); if (legacyAPIURLMatch) { const fullURL = new URL(req.url, this.address); const storeName = fullURL.searchParams.get("context") ?? LEGACY_DEFAULT_STORE; const key = legacyAPIURLMatch.groups?.key; const siteID = legacyAPIURLMatch.groups?.site_id; const urlPath = [siteID, storeName, key].filter(Boolean); const url = new URL(`/${urlPath.join("/")}?signature=${this.tokenHash}`, this.address); return { key, siteID, storeName, url, useSignedURL: true }; } return null; } sendResponse(req, res, status, body) { this.logDebug(`${req.method} ${req.url} ${status}`); res.writeHead(status); res.end(body); } async start() { await import_node_fs.promises.mkdir(this.directory, { recursive: true }); const server = import_node_http.default.createServer((req, res) => this.handleRequest(req, res)); this.server = server; return new Promise((resolve2, reject) => { server.listen(this.port, () => { const address = server.address(); if (!address || typeof address === "string") { return reject(new Error("Server cannot be started on a pipe or Unix socket")); } this.address = `http://localhost:${address.port}`; resolve2(address); }); }); } async stop() { if (!this.server) { return; } await new Promise((resolve2, reject) => { this.server?.close((error) => { if (error) { return reject(error); } resolve2(null); }); }); } validateAccess(req) { if (!this.token) { return true; } const { authorization = "" } = req.headers; if (authorization.toLowerCase().startsWith("bearer ") && authorization.slice("bearer ".length) === this.token) { return true; } if (!req.url) { return false; } const url = new URL(req.url, this.address); const signature = url.searchParams.get("signature"); if (signature === this.tokenHash) { return true; } return false; } /** * Traverses a path and collects both blobs and directories into a `result` * object, taking into account the `directories` and `prefix` parameters. */ static async walk(options) { const { directories, path, prefix, result, rootPath } = options; const entries = await import_node_fs.promises.readdir(path); for (const entry of entries) { const entryPath = (0, import_node_path.join)(path, entry); const stat = await import_node_fs.promises.stat(entryPath); let key = (0, import_node_path.relative)(rootPath, entryPath); if (import_node_path.sep !== "/") { key = key.split(import_node_path.sep).join("/"); } const mask = key.slice(0, prefix.length); const isMatch = prefix.startsWith(mask); if (!isMatch) { continue; } if (!stat.isDirectory()) { const etag = Math.random().toString().slice(2); result.blobs?.push({ etag, key, last_modified: stat.mtime.toISOString(), size: stat.size }); continue; } if (directories && key.startsWith(prefix)) { result.directories?.push(key); continue; } await _BlobsServer.walk({ directories, path: entryPath, prefix, rootPath, result }); } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { BlobsServer, Operation });