diff --git a/src/environment.ts b/src/environment.ts index 9be36e2..0522f53 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -1,5 +1,48 @@ -import { Buffer } from 'node:buffer' -import { env } from 'node:process' +import { base64Decode, base64Encode } from './util.ts' + +interface EnvironmentVariables { + delete: (key: string) => void + get: (key: string) => string | undefined + has: (key: string) => boolean + set: (key: string, value: string) => void + toObject: () => Record +} + +interface Globals { + Deno?: { + env: EnvironmentVariables + } + Netlify?: { + env: EnvironmentVariables + } + process?: { + env: Record + } +} + +/** + * Returns a cross-runtime interface for handling environment variables. It + * uses the `Netlify.env` global if available, otherwise looks for `Deno.env` + * and `process.env`. + */ +export const getEnvironment = (): EnvironmentVariables => { + const { Deno, Netlify, process } = globalThis as Globals + + return ( + Netlify?.env ?? + Deno?.env ?? { + delete: (key: string) => delete process?.env[key], + get: (key: string) => process?.env[key], + has: (key: string) => Boolean(process?.env[key]), + set: (key: string, value: string) => { + if (process?.env) { + process.env[key] = value + } + }, + toObject: () => process?.env ?? {}, + } + ) +} declare global { // Using `var` so that the declaration is hoisted in such a way that we can @@ -21,13 +64,13 @@ export interface EnvironmentContext { } export const getEnvironmentContext = (): EnvironmentContext => { - const context = globalThis.netlifyBlobsContext || env.NETLIFY_BLOBS_CONTEXT + const context = globalThis.netlifyBlobsContext || getEnvironment().get('NETLIFY_BLOBS_CONTEXT') if (typeof context !== 'string' || !context) { return {} } - const data = Buffer.from(context, 'base64').toString() + const data = base64Decode(context) try { return JSON.parse(data) as EnvironmentContext @@ -39,9 +82,9 @@ export const getEnvironmentContext = (): EnvironmentContext => { } export const setEnvironmentContext = (context: EnvironmentContext) => { - const encodedContext = Buffer.from(JSON.stringify(context)).toString('base64') + const encodedContext = base64Encode(JSON.stringify(context)) - env.NETLIFY_BLOBS_CONTEXT = encodedContext + getEnvironment().set('NETLIFY_BLOBS_CONTEXT', encodedContext) } export class MissingBlobsEnvironmentError extends Error { diff --git a/src/lambda_compat.ts b/src/lambda_compat.ts index a87fa6f..cbe3200 100644 --- a/src/lambda_compat.ts +++ b/src/lambda_compat.ts @@ -1,7 +1,6 @@ -import { Buffer } from 'node:buffer' - import { EnvironmentContext, setEnvironmentContext } from './environment.ts' import type { LambdaEvent } from './types.ts' +import { base64Decode } from './util.ts' interface BlobsEventData { token: string @@ -9,8 +8,8 @@ interface BlobsEventData { } export const connectLambda = (event: LambdaEvent) => { - const rawData = Buffer.from(event.blobs, 'base64') - const data = JSON.parse(rawData.toString('ascii')) as BlobsEventData + const rawData = base64Decode(event.blobs) + const data = JSON.parse(rawData) as BlobsEventData const environmentContext: EnvironmentContext = { deployID: event.headers['x-nf-deploy-id'], edgeURL: data.url, diff --git a/src/metadata.ts b/src/metadata.ts index 3e4ef22..185398b 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -1,4 +1,4 @@ -import { Buffer } from 'node:buffer' +import { base64Decode, base64Encode } from './util.ts' export type Metadata = Record @@ -12,7 +12,7 @@ export const encodeMetadata = (metadata?: Metadata) => { return null } - const encodedObject = Buffer.from(JSON.stringify(metadata)).toString('base64') + const encodedObject = base64Encode(JSON.stringify(metadata)) const payload = `b64;${encodedObject}` if (METADATA_HEADER_EXTERNAL.length + payload.length > METADATA_MAX_SIZE) { @@ -28,7 +28,7 @@ export const decodeMetadata = (header: string | null): Metadata => { } const encodedData = header.slice(BASE64_PREFIX.length) - const decodedData = Buffer.from(encodedData, 'base64').toString() + const decodedData = base64Decode(encodedData) const metadata = JSON.parse(decodedData) return metadata diff --git a/src/retry.ts b/src/retry.ts index 9d6f8f2..fa38167 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -1,8 +1,7 @@ -import { env } from 'node:process' - +import { getEnvironment } from './environment.ts' import type { Fetcher } from './types.ts' -const DEFAULT_RETRY_DELAY = env.NODE_ENV === 'test' ? 1 : 5000 +const DEFAULT_RETRY_DELAY = getEnvironment().get('NODE_ENV') === 'test' ? 1 : 5000 const MIN_RETRY_DELAY = 1000 const MAX_RETRY = 5 const RATE_LIMIT_HEADER = 'X-RateLimit-Reset' diff --git a/src/store.ts b/src/store.ts index 430f028..2371ada 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,5 +1,3 @@ -import { Buffer } from 'node:buffer' - import { ListResponse, ListResponseBlob } from './backend/list.ts' import { Client } from './client.ts' import type { ConsistencyMode } from './consistency.ts' @@ -341,7 +339,7 @@ export class Store { throw new Error('Blob key must not start with forward slash (/).') } - if (Buffer.byteLength(key, 'utf8') > 600) { + if (new TextEncoder().encode(key).length > 600) { throw new Error( 'Blob key must be a sequence of Unicode characters whose UTF-8 encoding is at most 600 bytes long.', ) @@ -363,7 +361,7 @@ export class Store { throw new Error('Store name must not contain forward slashes (/).') } - if (Buffer.byteLength(name, 'utf8') > 64) { + if (new TextEncoder().encode(name).length > 64) { throw new Error( 'Store name must be a sequence of Unicode characters whose UTF-8 encoding is at most 64 bytes long.', ) diff --git a/src/util.ts b/src/util.ts index 167c631..ddc4d90 100644 --- a/src/util.ts +++ b/src/util.ts @@ -19,3 +19,25 @@ export const collectIterator = async (iterator: AsyncIterable): Promise error instanceof Error export type Logger = (...message: unknown[]) => void + +export const base64Decode = (input: string) => { + // eslint-disable-next-line n/prefer-global/buffer + const { Buffer } = globalThis + + if (Buffer) { + return Buffer.from(input, 'base64').toString() + } + + return atob(input) +} + +export const base64Encode = (input: string) => { + // eslint-disable-next-line n/prefer-global/buffer + const { Buffer } = globalThis + + if (Buffer) { + return Buffer.from(input).toString('base64') + } + + return btoa(input) +}