UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

243 lines 7.55 kB
function hasControlCharacter(value) { for (const char of value) { const code = char.charCodeAt(0); if (code <= 31 || code === 127) return true; } return false; } /** * Assert that a storage key follows Beignet's provider-neutral key rules. * * Valid keys are non-empty relative object paths. They do not contain control * characters, backslashes, empty path segments, or `.` / `..` segments. * Providers may enforce additional adapter-specific restrictions after this * shared assertion. */ export function assertValidStorageKey(key) { if (key.length === 0) { throw new Error("Storage key must not be empty."); } if (hasControlCharacter(key)) { throw new Error("Storage key must not include control characters."); } if (key.startsWith("/")) { throw new Error("Storage key must not start with '/'."); } if (key.endsWith("/")) { throw new Error("Storage key must not end with '/'."); } if (key.includes("\\")) { throw new Error("Storage key must use '/' separators, not '\\'."); } const segments = key.split("/"); if (segments.some((segment) => segment === "")) { throw new Error("Storage key must not include empty path segments."); } if (segments.some((segment) => segment === "." || segment === "..")) { throw new Error("Storage key must not include '.' or '..' segments."); } } /** * Normalize and validate an optional storage key prefix. * * Empty and slash-only prefixes normalize to an empty string. */ export function normalizeStorageKeyPrefix(prefix) { if (!prefix) return ""; const normalized = prefix.replace(/^\/+|\/+$/g, ""); if (!normalized) return ""; assertValidStorageKey(normalized); return normalized; } /** * Prefix a storage key with an optional app or environment namespace. */ export function prefixStorageKey({ keyPrefix, key, }) { const normalizedPrefix = normalizeStorageKeyPrefix(keyPrefix); assertValidStorageKey(key); return normalizedPrefix ? `${normalizedPrefix}/${key}` : key; } /** * Format an encoded public URL for a validated storage key. */ export function createStoragePublicUrl({ publicBaseUrl, key, }) { assertValidStorageKey(key); const base = publicBaseUrl.replace(/\/+$/, ""); const encodedKey = key .split("/") .map((part) => encodeURIComponent(part)) .join("/"); return `${base}/${encodedKey}`; } function copyBytes(bytes) { return new Uint8Array(bytes); } function bytesToArrayBuffer(bytes) { const buffer = new ArrayBuffer(bytes.byteLength); new Uint8Array(buffer).set(bytes); return buffer; } function bytesToStream(bytes) { const copy = copyBytes(bytes); return new ReadableStream({ start(controller) { controller.enqueue(copy); controller.close(); }, }); } async function streamToBytes(stream) { const reader = stream.getReader(); const chunks = []; let size = 0; try { while (true) { const result = await reader.read(); if (result.done) break; chunks.push(result.value); size += result.value.byteLength; } } finally { reader.releaseLock(); } const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } return bytes; } async function storageBodyToBytes(body) { if (typeof body === "string") { return new TextEncoder().encode(body); } if (body instanceof Uint8Array) { return copyBytes(body); } if (body instanceof ArrayBuffer) { return new Uint8Array(body.slice(0)); } if (body instanceof Blob) { return new Uint8Array(await body.arrayBuffer()); } return streamToBytes(body); } function cloneObject(entry) { return { key: entry.key, size: entry.size, ...(entry.contentType !== undefined ? { contentType: entry.contentType } : {}), ...(entry.cacheControl !== undefined ? { cacheControl: entry.cacheControl } : {}), metadata: { ...entry.metadata }, visibility: entry.visibility, lastModified: new Date(entry.lastModified), }; } function createObjectBody(entry) { const object = cloneObject(entry); let bodyUsed = false; function consumeBytes() { if (bodyUsed) { throw new Error("Storage object body has already been consumed."); } bodyUsed = true; return copyBytes(entry.bytes); } return { ...object, get bodyUsed() { return bodyUsed; }, stream() { return bytesToStream(consumeBytes()); }, async bytes() { return consumeBytes(); }, async arrayBuffer() { return bytesToArrayBuffer(consumeBytes()); }, async text() { return new TextDecoder().decode(consumeBytes()); }, }; } /** * Create an in-memory object storage adapter for tests, examples, and * single-process development. * * This adapter validates object keys using Beignet's storage key rules. It is * not durable and does not share objects across processes. * * @param options - Optional public URL base for public objects. * @returns A storage port backed by a local `Map`. */ export function createMemoryStorage(options = {}) { const objects = new Map(); return { async put(key, body, putOptions) { assertValidStorageKey(key); const bytes = await storageBodyToBytes(body); const entry = { key, size: bytes.byteLength, ...(putOptions?.contentType !== undefined ? { contentType: putOptions.contentType } : {}), ...(putOptions?.cacheControl !== undefined ? { cacheControl: putOptions.cacheControl } : {}), metadata: { ...(putOptions?.metadata ?? {}) }, visibility: putOptions?.visibility ?? "private", lastModified: new Date(), bytes, }; objects.set(key, entry); return cloneObject(entry); }, async get(key) { assertValidStorageKey(key); const entry = objects.get(key); if (!entry) return null; return createObjectBody(entry); }, async stat(key) { assertValidStorageKey(key); const entry = objects.get(key); if (!entry) return null; return cloneObject(entry); }, async delete(key) { assertValidStorageKey(key); return objects.delete(key); }, async exists(key) { assertValidStorageKey(key); return objects.has(key); }, async publicUrl(key) { assertValidStorageKey(key); const entry = objects.get(key); if (entry?.visibility !== "public" || !options.publicBaseUrl) { return null; } return createStoragePublicUrl({ publicBaseUrl: options.publicBaseUrl, key, }); }, }; } //# sourceMappingURL=storage.js.map