@molecule/api-http-relay

Provider bond · http · API (Node) · v1.0.1 · Apache-2.0

HTTP client that sends outbound requests through the molecule.dev egress relay, for runtimes that cannot dial out (a server running in the browser): the relay applies the project egress allowlist, follows no redirect past its checks, and returns the upstream status in a header. Includes installRelayFetch, which routes the runtime's global fetch through it.

npm install @molecule/api-http-relay

npm · Source on GitHub · Implements @molecule/api-http

How it works

@molecule/api-http-relay is a provider bond on the API (Node) side: it implements the http core interface (@molecule/api-http) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setClient } from '@molecule/api-http'
import { installRelayFetch, isRelayError, provider } from '@molecule/api-http-relay'

// 1. The @molecule/api-http client.
setClient(provider)

// 2. Or patch fetch for every library at once (call once at startup).
const relay = installRelayFetch()

try {
  const res = await fetch('https://api.stripe.com/v1/charges', {
    method: 'POST',
    headers: { Authorization: `Bearer ${stripeKey}` },
    body: new URLSearchParams({ amount: '100', currency: 'usd' }),
  })
  console.log(res.status, await res.json()) // the upstream's real status and body
} catch (error) {
  // The relay's own refusal, verbatim, e.g. a 403 naming the blocked host.
  if (isRelayError(error) && error.kind === 'refused') console.error(error.status, error.message)
}

relay.uninstall() // restores the previous fetch

Works with: @molecule/api-http, @molecule/api-secrets

Secrets: MOLECULE_EGRESS_RELAY_URL, MOLECULE_EGRESS_CREDENTIAL

Reference

Egress relay HTTP client for molecule.dev — outbound HTTP for runtimes that cannot dial out.

A server running in a user's browser (or any runtime without raw sockets) cannot call Stripe, Anthropic or any other REST API directly: cross-origin rules refuse it. The molecule.dev platform serves a relay (POST /api/egress-relay) that makes the request under the project's egress credential and allowlist. This package is its client, in two forms:

  • provider — an @molecule/api-http client: setClient(provider) and every get/post/… call goes through the relay.
  • installRelayFetch() — replaces globalThis.fetch, so vendor SDKs that call fetch (Stripe, Anthropic, OpenAI…) go through the relay unchanged. Returns uninstall().

Both read MOLECULE_EGRESS_RELAY_URL and MOLECULE_EGRESS_CREDENTIAL unless relayUrl / credential are passed.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setClient } from '@molecule/api-http'
import { installRelayFetch, isRelayError, provider } from '@molecule/api-http-relay'

// 1. The @molecule/api-http client.
setClient(provider)

// 2. Or patch fetch for every library at once (call once at startup).
const relay = installRelayFetch()

try {
  const res = await fetch('https://api.stripe.com/v1/charges', {
    method: 'POST',
    headers: { Authorization: `Bearer ${stripeKey}` },
    body: new URLSearchParams({ amount: '100', currency: 'usd' }),
  })
  console.log(res.status, await res.json()) // the upstream's real status and body
} catch (error) {
  // The relay's own refusal, verbatim, e.g. a 403 naming the blocked host.
  if (isRelayError(error) && error.kind === 'refused') console.error(error.status, error.message)
}

relay.uninstall() // restores the previous fetch

Type

provider

Installation

npm install @molecule/api-http-relay @molecule/api-http @molecule/api-secrets

API

Interfaces

RelayOptions

Options shared by installRelayFetch, createRelayFetch and createRelayClient.

interface RelayOptions {
  /**
   * The relay endpoint, e.g. `https://api.molecule.dev/api/egress-relay`.
   * Defaults to the `MOLECULE_EGRESS_RELAY_URL` environment variable.
   */
  relayUrl?: string

  /**
   * The project's egress credential: the `<projectId>:<mac>` pair the platform mints
   * (it is base64-encoded into `Authorization: Basic …` for you), or an already
   * base64-encoded value. Defaults to the `MOLECULE_EGRESS_CREDENTIAL` environment variable.
   */
  credential?: string

  /**
   * The fetch used to reach the relay itself. Defaults to the `globalThis.fetch` in place
   * when the relay fetch is created (so installing the relay fetch never recurses).
   */
  fetch?: typeof fetch

  /**
   * Most redirects followed for one request (each hop goes back through the relay).
   * Defaults to 5.
   */
  maxRedirects?: number

  /**
   * Return `true` for a URL that must NOT be relayed (it goes to the underlying fetch
   * unchanged), e.g. the runtime's own origin. Requests to the relay URL itself are
   * never relayed, whatever this returns.
   */
  bypass?: (url: URL) => boolean
}

RelayFetch

A fetch-compatible function that sends http(s) requests through the relay.

interface RelayFetch {
  (input: string | URL | Request, init?: RequestInit): Promise<Response>
}

RelayFetchInstallation

Handle returned by installRelayFetch.

interface RelayFetchInstallation {
  /** The relay fetch now installed as `globalThis.fetch`. */
  fetch: RelayFetch
  /** Restore the fetch that was installed before (idempotent). */
  uninstall: () => void
}

ResolvedRelayConfig

The resolved relay settings.

interface ResolvedRelayConfig {
  /** The relay endpoint. */
  relayUrl: URL
  /** The `Authorization` header value for the relay. */
  authorization: string
}

Types

RelayErrorKind

Why a relayed request failed:

  • refused — the relay itself answered with a refusal (X-Relay-Error: 1): no or bad credential (401), egress policy denial (403), too many in flight (429), upstream unreachable / too slow / too large (502), a malformed request (400), too large a body (413).
  • unavailable — the relay could not be reached, or something in front of it answered (a non-200 without X-Relay-Error).
  • protocol — the answer did not follow the relay protocol (200 without X-Relay-Status).
  • redirect — a redirect could not be followed (too many, a non-http(s) target, or redirect: 'error').
  • config — no relay URL or credential is configured.
  • unsupported — the request cannot be relayed as given (e.g. a streaming body).
type RelayErrorKind = 'refused' | 'unavailable' | 'protocol' | 'redirect' | 'config' | 'unsupported'

Classes

RelayError

A failed relayed request. Extends TypeError so code that catches fetch network failures keeps working; for kind: 'refused' the message is the relay's own words, verbatim (policy denials are written to be read by a person or an agent).

Also satisfies the HttpError shape of @molecule/api-http when thrown by the provider.

class RelayError extends TypeError {
  /** Why it failed. */
  readonly kind: RelayErrorKind
  /** The relay's own HTTP status, when the relay answered. */
  readonly status?: number
  /** Machine-readable code, e.g. `RELAY_REFUSED`. */
  code: string
  /** The request, when thrown through the `@molecule/api-http` provider. */
  request?: HttpRequestOptions & { url: string }
  /** Always undefined: a relay failure has no upstream response. */
  response?: HttpResponse
  /** Whether the request was aborted. */
  isAborted?: boolean
  /** Whether the request timed out. */
  isTimeout?: boolean

  /**
   * Create a relay client error.
   *
   * @param kind - Why it failed.
   * @param message - What went wrong, in plain words (the relay's own text for `refused`).
   * @param status - The relay's HTTP status, when it answered.
   * @param options - Standard error options (`cause`).
   */
  constructor(kind: RelayErrorKind, message: string, status?: number, options?: ErrorOptions) {
    super(message, options)
    this.name = 'RelayError'
    this.kind = kind
    this.status = status
    this.code = `RELAY_${kind.toUpperCase()}`
  }
}

Functions

isRelayError(error)

Whether a value is a RelayError.

function isRelayError(error: unknown): boolean
  • error — Anything caught.

Returns: true for a relay client error.

createRelayClient(relayOptions)

Create an HttpClient that relays every request.

function createRelayClient(relayOptions?: RelayOptions): HttpClient
  • relayOptions — Relay URL, credential, underlying fetch, redirect limit. Unset values come from MOLECULE_EGRESS_RELAY_URL / MOLECULE_EGRESS_CREDENTIAL at request time.

Returns: The client.

createRelayFetch(options)

Create a fetch-compatible function that sends every http(s) request through the relay. Non-http(s) URLs, the relay URL itself and URLs matched by bypass go to the underlying fetch unchanged. Configuration is read on each call (options first, then the environment).

function createRelayFetch(options?: RelayOptions): RelayFetch
  • options — Relay URL, credential, underlying fetch, redirect limit, bypass.

Returns: The relay fetch.

installRelayFetch(options)

Replace globalThis.fetch with the relay fetch. Throws at once when no relay URL or credential is configured, rather than on the first request.

function installRelayFetch(options?: RelayOptions): RelayFetchInstallation
  • options — Relay URL, credential, underlying fetch, redirect limit, bypass.

Returns: The installed fetch and an uninstall() that restores the previous one (only if nothing replaced it since).

statusTextFor(status)

The standard reason phrase for a status (the relay does not carry the upstream's own).

function statusTextFor(status: number): string
  • status — HTTP status code.

Returns: The reason phrase, or '' when uncommon.

readEnv(name)

Read an environment variable without assuming process exists.

function readEnv(name: string): string | undefined
  • name — Variable name.

Returns: Its value, or undefined.

bytesToBase64(bytes)

Base64-encode bytes, portably.

function bytesToBase64(bytes: Uint8Array<ArrayBufferLike>): string
  • bytes — The bytes.

Returns: Standard base64 (with padding).

authorizationFor(credential)

Build the relay's Authorization header value from a credential. A <projectId>:<mac> pair is base64-encoded (what the relay verifies); a value without a colon is taken as already encoded; a leading Basic is tolerated.

function authorizationFor(credential: string): string
  • credential — The configured credential.

Returns: The header value, Basic <base64>.

resolveRelayConfig(options)

Resolve the relay URL and credential from options, then the environment.

function resolveRelayConfig(options: RelayOptions): ResolvedRelayConfig
  • options — Caller options.

Returns: The settings.

isRelayEndpoint(url, relayUrl)

Whether a URL addresses the relay endpoint itself (same origin and path; query ignored).

function isRelayEndpoint(url: URL, relayUrl: URL): boolean
  • url — The request URL.
  • relayUrl — The relay endpoint.

Returns: true when the request is for the relay.

upstreamHeaders(headers)

Copy the upstream's headers out of a relay answer, without the relay's own.

function upstreamHeaders(headers: Headers): Headers
  • headers — The relay answer's headers.

Returns: The upstream headers.

dropBodyHeaders(headers)

Remove the request-body headers (when a redirect turns the request into a GET).

function dropBodyHeaders(headers: Headers): void
  • headers — Headers to edit in place.

dropCrossOriginHeaders(headers)

Remove credentials that must not follow a redirect to another origin.

function dropCrossOriginHeaders(headers: Headers): void
  • headers — Headers to edit in place.

abortReason(signal)

The error a fetch rejects with on abort: the signal's reason, else an AbortError.

function abortReason(signal: AbortSignal): unknown
  • signal — The aborted signal.

Returns: The rejection value.

Constants

relayClient

The relay client, configured from MOLECULE_EGRESS_RELAY_URL and MOLECULE_EGRESS_CREDENTIAL at request time.

const relayClient: HttpClient

provider

Default relay HTTP provider: setClient(provider) from @molecule/api-http.

const provider: HttpClient

httpRelaySecretDefinitions

Secret definitions required by the egress relay HTTP bond.

const httpRelaySecretDefinitions: SecretDefinition[]

RELAY_URL_ENV

Environment variable holding the relay endpoint URL.

const RELAY_URL_ENV: 'MOLECULE_EGRESS_RELAY_URL'

RELAY_CREDENTIAL_ENV

Environment variable holding the project's egress credential.

const RELAY_CREDENTIAL_ENV: 'MOLECULE_EGRESS_CREDENTIAL'

DEFAULT_MAX_REDIRECTS

Default for maxRedirects.

const DEFAULT_MAX_REDIRECTS: 5

REDIRECT_STATUSES

Statuses a fetch follows as redirects.

const REDIRECT_STATUSES: Set<number>

NULL_BODY_STATUSES

Statuses whose response never has a body.

const NULL_BODY_STATUSES: Set<number>

Core Interface

Implements @molecule/api-http interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setClient } from '@molecule/api-http'
import { provider } from '@molecule/api-http-relay'

export function setupHttpRelay(): void {
  setClient(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-http ^1.0.1
  • @molecule/api-secrets ^1.0.1

Environment Variables

  • MOLECULE_EGRESS_RELAY_URL (required) — Egress relay URL
    • Setup: The molecule.dev egress relay endpoint outbound HTTP is sent to.
    • Example: https://api.molecule.dev/api/egress-relay
  • MOLECULE_EGRESS_CREDENTIAL (required) — Project egress credential
    • Setup: The credential the platform minted for this sandbox (<projectId>:<mac>). Never commit it.
    • Example: <projectId>:<mac>

Runtime Dependencies

  • @molecule/api-http

  • @molecule/api-secrets

  • Bodies are buffered, never streamed. The relay carries the whole request body as base64 in JSON (5 MB limit) and returns the whole response (10 MB limit, 30 s per request). A ReadableStream request body is REJECTED with a RelayError (kind: 'unsupported') rather than silently truncated — buffer it into a string, Uint8Array, Blob, FormData or URLSearchParams first. Response bodies arrive complete, so streaming APIs (server-sent events, stream: true AI calls) only deliver once the upstream finishes; prefer their non-streaming mode.

  • Redirects are followed by THIS client, through the relay, never by the relay and never directly: 301/302/303 become a GET without a body (HEAD stays HEAD), 307/308 keep method and body, at most 5 hops (maxRedirects), a hop to a non-http(s) URL is refused, and Authorization/Cookie are dropped when a redirect changes origin. redirect: 'manual' returns the 3xx Response itself; redirect: 'error' rejects.

  • A relay refusal throws RelayError (a TypeError subclass, so code that catches fetch failures still works). kind: 'refused' carries the relay's own message verbatim and its HTTP status: 401 (missing or invalid credential), 403 (the egress policy blocked the host — the message names it; the project owner can allow it), 429 (too many requests in flight), 502 (upstream unreachable, too slow or too large), 400/413 (bad or oversize request). An upstream error status (a Stripe 402, an API 500) is NOT a RelayError: the Response simply has that status (and the @molecule/api-http provider throws its usual HttpError for non-2xx).

  • Allowed methods are the relay's: GET, HEAD, POST, PUT, PATCH, DELETE; ports 80 and 443 only; no credentials embedded in the URL. Set-Cookie never comes back.

  • Non-http(s) URLs (data:, blob:), requests to the relay URL itself and URLs matched by the bypass option go to the original fetch untouched.

  • Response.statusText is the standard reason phrase for the status (the relay does not carry the upstream's own wording).

  • The credential and request bodies are never logged.