@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-relaynpm · 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 fetchWorks 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-httpclient:setClient(provider)and everyget/post/… call goes through the relay.installRelayFetch()— replacesglobalThis.fetch, so vendor SDKs that callfetch(Stripe, Anthropic, OpenAI…) go through the relay unchanged. Returnsuninstall().
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.tsJSDoc, 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 withoutX-Relay-Error).protocol— the answer did not follow the relay protocol (200 withoutX-Relay-Status).redirect— a redirect could not be followed (too many, a non-http(s) target, orredirect: '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 fromMOLECULE_EGRESS_RELAY_URL/MOLECULE_EGRESS_CREDENTIALat 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
ReadableStreamrequest body is REJECTED with aRelayError(kind: 'unsupported') rather than silently truncated — buffer it into a string,Uint8Array,Blob,FormDataorURLSearchParamsfirst. Response bodies arrive complete, so streaming APIs (server-sent events,stream: trueAI 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, andAuthorization/Cookieare dropped when a redirect changes origin.redirect: 'manual'returns the 3xxResponseitself;redirect: 'error'rejects. -
A relay refusal throws
RelayError(aTypeErrorsubclass, 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 aRelayError: theResponsesimply has that status (and the@molecule/api-httpprovider throws its usualHttpErrorfor 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-Cookienever comes back. -
Non-http(s) URLs (
data:,blob:), requests to the relay URL itself and URLs matched by thebypassoption go to the original fetch untouched. -
Response.statusTextis 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.