diff --git a/src/cloudflare/internal/hyperdrive-mysql-api.ts b/src/cloudflare/internal/hyperdrive-mysql-api.ts new file mode 100644 index 00000000000..3bc373f9c48 --- /dev/null +++ b/src/cloudflare/internal/hyperdrive-mysql-api.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import { Hyperdrive, type HyperdriveEnv } from 'cloudflare-internal:hyperdrive'; + +export default function makeBinding(env: HyperdriveEnv): Hyperdrive { + return new Hyperdrive(env, 'mysql'); +} diff --git a/src/cloudflare/internal/hyperdrive-postgres-api.ts b/src/cloudflare/internal/hyperdrive-postgres-api.ts new file mode 100644 index 00000000000..e0dd4fbec21 --- /dev/null +++ b/src/cloudflare/internal/hyperdrive-postgres-api.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import { Hyperdrive, type HyperdriveEnv } from 'cloudflare-internal:hyperdrive'; + +export default function makeBinding(env: HyperdriveEnv): Hyperdrive { + return new Hyperdrive(env, 'postgresql'); +} diff --git a/src/cloudflare/internal/hyperdrive.ts b/src/cloudflare/internal/hyperdrive.ts new file mode 100644 index 00000000000..f817626236e --- /dev/null +++ b/src/cloudflare/internal/hyperdrive.ts @@ -0,0 +1,104 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import entrypoints from 'cloudflare-internal:workers'; +import wrappedBinding from 'cloudflare-internal:wrapped-binding'; +import { type Socket } from 'cloudflare-internal:sockets'; + +// Shared implementation of the Hyperdrive bindings. Each database scheme has its own wrapped binding +// module (`cloudflare-internal:hyperdrive-postgres-api` and `cloudflare-internal:hyperdrive-mysql-api`) +// so that the scheme, and therefore the port, is implied by the module name. The module name is +// preserved when a wrapped binding is serialized, so the binding's only inner binding is `fetcher`. + +// A service binding to the Hyperdrive RPC worker. A random virtual host is mapped to it, so that +// `cloudflare:sockets` connect() calls to that host and the database port route through it. +interface HyperdriveFetcher { + connect(address: string): Socket; +} + +export interface HyperdriveEnv { + fetcher: HyperdriveFetcher; +} + +export type HyperdriveScheme = 'postgresql' | 'mysql'; + +const PORTS: Record = { + postgresql: 5432, + mysql: 3306, +}; + +// 16 random bytes as 32 lowercase hex characters, which need no escaping in a connection string. +function randomHex(): string { + return Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => + b.toString(16).padStart(2, '0') + ).join(''); +} + +export class Hyperdrive extends wrappedBinding.WrappedBinding { + readonly #fetcher: HyperdriveFetcher; + readonly #port: number; + readonly #scheme: HyperdriveScheme; + readonly #host: string; + #database: string | undefined; + #user: string | undefined; + #password: string | undefined; + + declare readonly database: string; + declare readonly user: string; + declare readonly password: string; + declare readonly scheme: HyperdriveScheme; + declare readonly connectionString: string; + declare readonly host: string; + declare readonly port: number; + declare readonly connect: () => Socket; + + constructor(env: HyperdriveEnv, scheme: HyperdriveScheme) { + super(env.fetcher); + this.#fetcher = env.fetcher; + this.#scheme = scheme; + this.#port = PORTS[scheme]; + const authority = entrypoints.mapVirtualHost(this.#fetcher, this.#port); + this.#host = authority.slice(0, authority.lastIndexOf(':')); + + // Own, read-only properties, matching the shape of the native Hyperdrive binding. + Object.defineProperties(this, { + database: { enumerable: true, get: (): string => this.#getDatabase() }, + user: { enumerable: true, get: (): string => this.#getUser() }, + password: { enumerable: true, get: (): string => this.#getPassword() }, + scheme: { enumerable: true, get: (): HyperdriveScheme => this.#scheme }, + connectionString: { + enumerable: true, + get: (): string => this.#getConnectionString(), + }, + host: { enumerable: true, get: (): string => this.#host }, + port: { enumerable: true, get: (): number => this.#port }, + connect: { + value: (): Socket => + this.#fetcher.connect(`${this.#host}:${this.#port}`), + }, + }); + } + + // The worker never sees the real database credentials; the Hyperdrive RPC worker holds them and + // the binding exposes independent random placeholders instead. Each is generated once per binding, + // on first access: bindings are constructed at global scope, where generating random values is + // disallowed. + #getDatabase(): string { + return (this.#database ??= randomHex()); + } + + #getUser(): string { + return (this.#user ??= randomHex()); + } + + #getPassword(): string { + return (this.#password ??= randomHex()); + } + + #getConnectionString(): string { + const sslOption = + this.#scheme === 'postgresql' ? 'sslmode=disable' : 'ssl-mode=disabled'; + return `${this.#scheme}://${this.#getUser()}:${this.#getPassword()}@${this.#host}:${this.#port}/${this.#getDatabase()}?${sslOption}`; + } +} diff --git a/src/cloudflare/internal/test/hyperdrive/BUILD.bazel b/src/cloudflare/internal/test/hyperdrive/BUILD.bazel new file mode 100644 index 00000000000..7cf5f66cace --- /dev/null +++ b/src/cloudflare/internal/test/hyperdrive/BUILD.bazel @@ -0,0 +1,7 @@ +load("//:build/wd_test.bzl", "wd_test") + +wd_test( + src = "hyperdrive-test.wd-test", + args = ["--experimental"], + data = glob(["*.js"]), +) diff --git a/src/cloudflare/internal/test/hyperdrive/hyperdrive-mock.js b/src/cloudflare/internal/test/hyperdrive/hyperdrive-mock.js new file mode 100644 index 00000000000..629170d125b --- /dev/null +++ b/src/cloudflare/internal/test/hyperdrive/hyperdrive-mock.js @@ -0,0 +1,17 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// Simulates the Hyperdrive RPC worker's TCP side: replies with the authority the socket was +// opened against, then echoes everything written to it. +export default { + async connect(socket) { + const { localAddress } = await socket.opened; + const writer = socket.writable.getWriter(); + await writer.write(new TextEncoder().encode(`${localAddress}\n`)); + for await (const chunk of socket.readable) { + await writer.write(chunk); + } + await writer.close(); + }, +}; diff --git a/src/cloudflare/internal/test/hyperdrive/hyperdrive-test.js b/src/cloudflare/internal/test/hyperdrive/hyperdrive-test.js new file mode 100644 index 00000000000..08f1015c14b --- /dev/null +++ b/src/cloudflare/internal/test/hyperdrive/hyperdrive-test.js @@ -0,0 +1,246 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import assert from 'node:assert'; +import { connect } from 'cloudflare:sockets'; +import { WorkerEntrypoint } from 'cloudflare:workers'; + +const HOST_PATTERN = /^[0-9a-f]{32}\.workers\.alt$/; +const CREDENTIAL_PATTERN = /^[0-9a-f]{32}$/; + +const PROPERTY_NAMES = [ + 'database', + 'user', + 'password', + 'scheme', + 'connectionString', + 'host', + 'port', +]; + +async function readLine(reader) { + const decoder = new TextDecoder(); + let text = ''; + while (!text.includes('\n')) { + const { value, done } = await reader.read(); + if (done) break; + text += decoder.decode(value, { stream: true }); + } + const newline = text.indexOf('\n'); + return { line: text.slice(0, newline), rest: text.slice(newline + 1) }; +} + +export const postgresProperties = { + test(_ctrl, env) { + const hd = env.HYPERDRIVE_PG; + assert.match(hd.database, CREDENTIAL_PATTERN); + assert.match(hd.user, CREDENTIAL_PATTERN); + assert.strictEqual(hd.scheme, 'postgresql'); + assert.strictEqual(hd.port, 5432); + assert.match(hd.host, HOST_PATTERN); + // The host is stable across reads. + assert.strictEqual(hd.host, hd.host); + }, +}; + +export const mysqlProperties = { + test(_ctrl, env) { + const hd = env.HYPERDRIVE_MYSQL; + assert.match(hd.database, CREDENTIAL_PATTERN); + assert.match(hd.user, CREDENTIAL_PATTERN); + assert.strictEqual(hd.scheme, 'mysql'); + assert.strictEqual(hd.port, 3306); + assert.match(hd.host, HOST_PATTERN); + }, +}; + +// database, user, and password are independent random placeholders: the real credentials only +// reach the Hyperdrive RPC worker. They are not embedded in the host. +export const generatedCredentials = { + test(_ctrl, env) { + const pg = env.HYPERDRIVE_PG; + const mysql = env.HYPERDRIVE_MYSQL; + for (const name of ['database', 'user', 'password']) { + assert.ok(!pg.host.includes(pg[name]), name); + assert.match(pg[name], CREDENTIAL_PATTERN, name); + assert.match(mysql[name], CREDENTIAL_PATTERN, name); + // Generated once per binding instance and then stable. + assert.strictEqual(pg[name], pg[name], name); + // Independent per binding instance. + assert.notStrictEqual(pg[name], mysql[name], name); + } + // Independent of each other. + assert.strictEqual(new Set([pg.database, pg.user, pg.password]).size, 3); + }, +}; + +export const postgresConnectionString = { + test(_ctrl, env) { + const hd = env.HYPERDRIVE_PG; + assert.strictEqual( + hd.connectionString, + `postgresql://${hd.user}:${hd.password}@${hd.host}:5432/${hd.database}?sslmode=disable` + ); + + const url = new URL(hd.connectionString); + assert.strictEqual(url.protocol, 'postgresql:'); + assert.strictEqual(url.username, hd.user); + assert.strictEqual(url.password, hd.password); + assert.strictEqual(url.hostname, hd.host); + assert.strictEqual(url.port, '5432'); + assert.strictEqual(url.pathname, `/${hd.database}`); + assert.strictEqual(url.searchParams.get('sslmode'), 'disable'); + }, +}; + +export const mysqlConnectionString = { + test(_ctrl, env) { + const hd = env.HYPERDRIVE_MYSQL; + assert.strictEqual( + hd.connectionString, + `mysql://${hd.user}:${hd.password}@${hd.host}:3306/${hd.database}?ssl-mode=disabled` + ); + + // Drivers parse the generated credentials back out unchanged; hex needs no URL escaping. + const url = new URL(hd.connectionString); + assert.strictEqual(url.protocol, 'mysql:'); + assert.strictEqual(url.username, hd.user); + assert.strictEqual(url.password, hd.password); + assert.strictEqual(url.hostname, hd.host); + assert.strictEqual(url.port, '3306'); + assert.strictEqual(url.pathname, `/${hd.database}`); + assert.strictEqual(url.searchParams.get('ssl-mode'), 'disabled'); + }, +}; + +export const propertyShape = { + test(_ctrl, env) { + const hd = env.HYPERDRIVE_PG; + + // Data properties are own, enumerable, read-only accessors; connect is not enumerable. + assert.deepStrictEqual(Object.keys(hd), PROPERTY_NAMES); + for (const name of PROPERTY_NAMES) { + const desc = Object.getOwnPropertyDescriptor(hd, name); + assert.ok(desc, name); + assert.strictEqual(desc.enumerable, true, name); + assert.strictEqual(desc.configurable, false, name); + assert.strictEqual(typeof desc.get, 'function', name); + assert.strictEqual(desc.set, undefined, name); + } + + const connectDesc = Object.getOwnPropertyDescriptor(hd, 'connect'); + assert.ok(connectDesc); + assert.strictEqual(typeof connectDesc.value, 'function'); + assert.strictEqual(connectDesc.enumerable, false); + assert.strictEqual(connectDesc.writable, false); + assert.strictEqual(connectDesc.configurable, false); + + // Modules are strict, so writes to read-only properties throw. + assert.throws(() => { + hd.password = 'nope'; + }, TypeError); + assert.throws(() => { + hd.connect = () => {}; + }, TypeError); + assert.throws(() => { + delete hd.host; + }, TypeError); + + // connect is a single stable function that can't be redefined. + assert.strictEqual(hd.connect, hd.connect); + assert.throws( + () => Object.defineProperty(hd, 'connect', { value: () => {} }), + TypeError + ); + + // The inner fetcher is not exposed: no fetch(), no RPC wildcard, no private helpers. + assert.strictEqual(hd.fetch, undefined); + assert.strictEqual(hd.unexpectedRpcMethod, undefined); + assert.strictEqual('getPassword' in hd, false); + assert.strictEqual('fetcher' in hd, false); + + // Private state isn't leaked through JSON or spread. + assert.deepStrictEqual(Object.keys({ ...hd }), PROPERTY_NAMES); + assert.deepStrictEqual(Object.keys(JSON.parse(JSON.stringify(hd))), [ + ...PROPERTY_NAMES, + ]); + }, +}; + +async function testConnect(socket, hd) { + await socket.opened; + + const writer = socket.writable.getWriter(); + const reader = socket.readable.getReader(); + + // The mock reports the authority the socket was opened against. + const { line, rest } = await readLine(reader); + assert.strictEqual(line, `${hd.host}:${hd.port}`); + + // Round-trip data through the socket. + const payload = 'SELECT 1;'; + await writer.write(new TextEncoder().encode(payload)); + const decoder = new TextDecoder(); + let echoed = rest; + while (echoed.length < payload.length) { + const { value, done } = await reader.read(); + if (done) break; + echoed += decoder.decode(value, { stream: true }); + } + assert.strictEqual(echoed, payload); + + await writer.close(); + reader.releaseLock(); + await socket.close(); +} + +export const postgresConnect = { + async test(_ctrl, env) { + // connect() is usable detached from the binding. + const { connect } = env.HYPERDRIVE_PG; + await testConnect(connect(), env.HYPERDRIVE_PG); + }, +}; + +export const mysqlConnect = { + async test(_ctrl, env) { + const { connect } = env.HYPERDRIVE_MYSQL; + await testConnect(connect(), env.HYPERDRIVE_MYSQL); + }, +}; + +// The host is a virtual host mapped to the Hyperdrive service, so drivers that connect through +// `cloudflare:sockets` reach it too. +export const socketsConnect = { + async test(_ctrl, env) { + for (const hd of [env.HYPERDRIVE_PG, env.HYPERDRIVE_MYSQL]) { + await testConnect(connect(`${hd.host}:${hd.port}`), hd); + } + }, +}; + +// Receives Hyperdrive bindings over RPC. The wrapped binding is rebuilt from its fetcher and module +// name alone, so the scheme and port must come from the module name. +export class Receiver extends WorkerEntrypoint { + async inspect(hd) { + assert.match(hd.host, HOST_PATTERN); + assert.match(hd.database, CREDENTIAL_PATTERN); + await testConnect(hd.connect(), hd); + await testConnect(connect(`${hd.host}:${hd.port}`), hd); + return { scheme: hd.scheme, port: hd.port }; + } +} + +export const rpcSerialization = { + async test(_ctrl, env) { + assert.deepStrictEqual(await env.RECEIVER.inspect(env.HYPERDRIVE_PG), { + scheme: 'postgresql', + port: 5432, + }); + assert.deepStrictEqual(await env.RECEIVER.inspect(env.HYPERDRIVE_MYSQL), { + scheme: 'mysql', + port: 3306, + }); + }, +}; diff --git a/src/cloudflare/internal/test/hyperdrive/hyperdrive-test.wd-test b/src/cloudflare/internal/test/hyperdrive/hyperdrive-test.wd-test new file mode 100644 index 00000000000..bb5dc744f17 --- /dev/null +++ b/src/cloudflare/internal/test/hyperdrive/hyperdrive-test.wd-test @@ -0,0 +1,42 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + ( name = "hyperdrive-test", + worker = ( + modules = [ + (name = "worker", esModule = embed "hyperdrive-test.js") + ], + compatibilityFlags = ["nodejs_compat", "rpc"], + bindings = [ + ( name = "HYPERDRIVE_PG", + wrapped = ( + moduleName = "cloudflare-internal:hyperdrive-postgres-api", + innerBindings = [ + (name = "fetcher", service = "hyperdrive-mock"), + ], + ) + ), + ( name = "HYPERDRIVE_MYSQL", + wrapped = ( + moduleName = "cloudflare-internal:hyperdrive-mysql-api", + innerBindings = [ + (name = "fetcher", service = "hyperdrive-mock"), + ], + ) + ), + ( name = "RECEIVER", + service = (name = "hyperdrive-test", entrypoint = "Receiver") ), + ], + ) + ), + ( name = "hyperdrive-mock", + worker = ( + modules = [ + (name = "worker", esModule = embed "hyperdrive-mock.js") + ], + compatibilityFlags = ["nodejs_compat"], + ) + ), + ], +); diff --git a/src/cloudflare/internal/workers.d.ts b/src/cloudflare/internal/workers.d.ts index ffd43b764c2..226aed9b23d 100644 --- a/src/cloudflare/internal/workers.d.ts +++ b/src/cloudflare/internal/workers.d.ts @@ -63,6 +63,9 @@ export interface CacheContext { export function getCtxCache(): CacheContext | undefined; +// Like ctx.mapVirtualHost(). Returns `host:port`. +export function mapVirtualHost(fetcher: ServiceStub, port: number): string; + export function abortIsolate(reason?: string): never; export function retryable(...methods: ((...args: never[]) => unknown)[]): void; diff --git a/src/workerd/api/global-scope.c++ b/src/workerd/api/global-scope.c++ index 33312465004..ef11fb6fbed 100644 --- a/src/workerd/api/global-scope.c++ +++ b/src/workerd/api/global-scope.c++ @@ -162,13 +162,8 @@ jsg::Optional> ExecutionContext::getAccess(jsg::Lock& js kj::String ExecutionContext::mapVirtualHost( jsg::Lock& js, jsg::Ref fetcher, uint16_t port) { - // Set up override, or return existing magic hostname if override already exists. Overrides - // created using mapVirtualHost() should never use the legacy hyperdrive hostname, so the only way - // for that hostname to be used is if an override was already registered earlier using - // ExtendedFetcher. - fetcher->registerOverride(js, IsHyperdrive::NO, port); - return kj::str(KJ_ASSERT_NONNULL(fetcher->getHostInternal()), ":", - KJ_ASSERT_NONNULL(fetcher->getPortInternal())); + // Set up override, or return existing magic hostname if override already exists. + return fetcher->mapVirtualHost(js, port); } void ExecutionContext::abort(jsg::Lock& js, jsg::Optional reason) { diff --git a/src/workerd/api/http.c++ b/src/workerd/api/http.c++ index 87d925d213c..e3fc73747ff 100644 --- a/src/workerd/api/http.c++ +++ b/src/workerd/api/http.c++ @@ -2327,18 +2327,21 @@ jsg::Ref Fetcher::connect( return connectImpl(js, JSG_THIS, kj::mv(address), kj::mv(options)); } -void Fetcher::registerOverride(jsg::Lock& js, IsHyperdrive isHyperdrive, uint16_t overridePort) { +void Fetcher::registerOverride(jsg::Lock& js, uint16_t overridePort) { if (registeredConnectOverride) { return; } KJ_DASSERT(host == kj::none); kj::FixedArray randomBytes; workerd::getEntropy(randomBytes); - host = kj::str(kj::encodeHex(randomBytes), isHyperdrive ? ".hyperdrive.local" : ".workers.alt"); + host = kj::str(kj::encodeHex(randomBytes), ".workers.alt"); // Register an entry in the global scope connectOverrides HashMap so that cloudflare:sockets's - // connect() will route connections to this magic hostname through the fetcher. - auto& globalScope = IoContext::current().getCurrentLock().getGlobalScope(); + // connect() will route connections to this magic hostname through the fetcher. The global scope + // is per-isolate, so this also works outside of a request, e.g. while constructing bindings. + auto context = js.v8Context(); + auto& globalScope = jsg::extractInternalPointer( + js.v8Isolate, context, context->Global(), jsg::kNonResourceWrappableTagRange); globalScope.setConnectOverride( kj::str(KJ_ASSERT_NONNULL(host), ":", overridePort), [self = JSG_THIS](jsg::Lock& js) mutable { @@ -2352,10 +2355,15 @@ void Fetcher::registerOverride(jsg::Lock& js, IsHyperdrive isHyperdrive, uint16_ registeredConnectOverride = true; } +kj::String Fetcher::mapVirtualHost(jsg::Lock& js, uint16_t port) { + registerOverride(js, port); + return kj::str(KJ_ASSERT_NONNULL(host), ":", KJ_ASSERT_NONNULL(this->port)); +} + kj::StringPtr ExtendedFetcher::getHost(jsg::Lock& js) { // Ensures the connect override is registered on the global scope using registerOverride(), then // returns the random hostname. - registerOverride(js, isHyperdrive, KJ_ASSERT_NONNULL(port)); + registerOverride(js, KJ_ASSERT_NONNULL(port)); return KJ_ASSERT_NONNULL(host); } diff --git a/src/workerd/api/http.h b/src/workerd/api/http.h index 3b101b5aa4b..2b8bb440d59 100644 --- a/src/workerd/api/http.h +++ b/src/workerd/api/http.h @@ -26,8 +26,6 @@ namespace workerd::api { -WD_STRONG_BOOL(IsHyperdrive); - // Whether a Fetcher is exempt from the `fetcher_rpc` compatibility gate that Fetcher::getRpcMethod // applies, so its JSRPC wildcard resolves regardless of the worker's compatibility date. @@ -549,10 +547,12 @@ class Fetcher: public JsRpcClientProvider { JSG_SERIALIZABLE(rpc::SerializationTag::SERVICE_STUB); - // Set up a connection string override for a random host and the given port. isHyperdrive - // signifies whether the legacy hostname for hyperdrive should be used (only used with - // ExtendedFetcher). - void registerOverride(jsg::Lock& js, IsHyperdrive isHyperdrive, uint16_t overridePort); + // Set up a connection string override for a random `.workers.alt` host and the given port. + void registerOverride(jsg::Lock& js, uint16_t overridePort); + + // Registers the connection string override (if not already registered) and returns the mapped + // `host:port`. Shared by ctx.mapVirtualHost() and the internal Hyperdrive binding. + kj::String mapVirtualHost(jsg::Lock& js, uint16_t port); kj::Maybe getPortInternal() const { return port; @@ -610,11 +610,9 @@ class ExtendedFetcher final: public Fetcher { explicit ExtendedFetcher(uint channel, RequiresHostAndProtocol requiresHost, uint16_t connectionStringOverridePort, - IsHyperdrive isHyperdrive, bool isInHouse = false, RpcCompatGateBypassed rpcCompatGateBypassed = RpcCompatGateBypassed::NO) - : Fetcher(channel, requiresHost, isInHouse, rpcCompatGateBypassed), - isHyperdrive(isHyperdrive) { + : Fetcher(channel, requiresHost, isInHouse, rpcCompatGateBypassed) { port = connectionStringOverridePort; } @@ -637,9 +635,6 @@ class ExtendedFetcher final: public Fetcher { } JSG_ONEWAY_SERIALIZABLE(rpc::SerializationTag::SERVICE_STUB); - - private: - IsHyperdrive isHyperdrive; }; // Type of the second parameter to Request's constructor. Also the type of the second parameter diff --git a/src/workerd/api/tests/js-rpc-test.js b/src/workerd/api/tests/js-rpc-test.js index 148b19c0b2f..3e262f53a34 100644 --- a/src/workerd/api/tests/js-rpc-test.js +++ b/src/workerd/api/tests/js-rpc-test.js @@ -2387,7 +2387,6 @@ export let mapVirtualHostIdempotent = { async test(controller, env, ctx) { let host1 = ctx.mapVirtualHost(env.MyService, 5432); assert.ok(typeof host1 === 'string'); - // Overrides that are first created using mapVirtualHost() always use .workers.alt. assert.match(host1, /^[0-9a-f]{32}\.workers\.alt:5432$/); // Once the override is installed, it doesn't change in future calls. diff --git a/src/workerd/api/workers-module.c++ b/src/workerd/api/workers-module.c++ index acf2c43658e..34264ee38dc 100644 --- a/src/workerd/api/workers-module.c++ +++ b/src/workerd/api/workers-module.c++ @@ -68,6 +68,11 @@ jsg::Optional> EntrypointsModule::getCtxCache(jsg::Lock& return kj::none; } +kj::String EntrypointsModule::mapVirtualHost( + jsg::Lock& js, jsg::Ref fetcher, uint16_t port) { + return fetcher->mapVirtualHost(js, port); +} + void EntrypointsModule::abortIsolate(jsg::Lock& js, jsg::Optional reason) { auto& r = reason.orDefault(nullptr); KJ_IF_SOME(context, IoContext::tryCurrent()) { diff --git a/src/workerd/api/workers-module.h b/src/workerd/api/workers-module.h index 97a20fc3fdb..50ffef1b0e7 100644 --- a/src/workerd/api/workers-module.h +++ b/src/workerd/api/workers-module.h @@ -86,6 +86,11 @@ class EntrypointsModule: public jsg::Object { // expose an importable `cache` proxy. jsg::Optional> getCtxCache(jsg::Lock& js); + // Equivalent of ctx.mapVirtualHost() for internal modules, which have no access to ctx. Returns + // `host:port`. Can be called outside of a request, so bindings can map their host when they are + // constructed. + kj::String mapVirtualHost(jsg::Lock& js, jsg::Ref fetcher, uint16_t port); + // Immediately condemn and terminate the current isolate. In workerd for now this just aborts the // process. void abortIsolate(jsg::Lock& js, jsg::Optional reason); @@ -109,6 +114,7 @@ class EntrypointsModule: public jsg::Object { JSG_METHOD(waitUntil); JSG_METHOD(getCtxCache); + JSG_METHOD(mapVirtualHost); JSG_LAZY_INSTANCE_PROPERTY(restore, getRestoreSymbol); JSG_METHOD(abortIsolate); JSG_METHOD(retryable);