uvlink
v0.4.0
Published
A browser bridge for AWS Lambda MicroVMs — reach a MicroVM endpoint from a browser without hand-rolling a proxy. Injects the X-aws-proxy-auth token for HTTP and the lambda-microvms subprotocols for WebSockets, and auto-refreshes the token. Personal hobby
Maintainers
Readme
uvlink
Reach an AWS Lambda MicroVM from a browser — without hand-rolling a proxy.
A MicroVM endpoint requires a credential a browser can't attach: HTTP needs the
X-aws-proxy-auth header, and WebSockets carry the token in a custom
lambda-microvms.* subprotocol. So every interactive app (notebooks, web IDEs,
dashboards, terminals) ends up writing the same little auth-injecting proxy.
uvlink is that proxy, done once, properly:
- ✅ Injects the auth token for HTTP (
X-aws-proxy-auth+X-aws-proxy-port) - ✅ Injects the WebSocket subprotocols, preserving your app's own subprotocol
- ✅ Auto-refreshes the token before its 60-minute expiry (long sessions just work)
- ✅ Zero runtime dependencies — Node built-ins only
- ✅ Use it as a CLI (
npx) or a library
⚠️ Disclaimer
This is a personal hobby project. It is NOT an official product of, affiliated
with, supported by, or endorsed by AWS, AWS Lambda, or Amazon in any way. It is
maintained on a best-effort, as-is basis by an individual in their personal
capacity. The names "AWS", "Lambda", and "MicroVMs" are used only descriptively to
indicate what the tool interoperates with; all trademarks belong to their
respective owners. Provided under the MIT license with no warranty (see
LICENSE). Use at your own risk.
Install
npm install uvlink # as a library
# or just run it, no install:
npx uvlink --helpQuick start (CLI)
Point it at a running MicroVM; it looks up the endpoint, mints + refreshes the token, and serves a browser-friendly local URL:
npx uvlink --microvm-id microvm-0123… --region us-east-1 --port 3000
# ✓ open http://localhost:3000 in your browserOr supply the endpoint and token yourself (no AWS calls made by the tool):
npx uvlink --endpoint xxxx.lambda-microvm.us-east-1.on.aws --token <jwe>Quick start (library)
import { createProxy } from 'uvlink';
const proxy = createProxy({
endpoint: 'xxxx.lambda-microvm.us-east-1.on.aws',
microvmId: 'microvm-0123…', // self-mints + refreshes the token via the aws CLI
region: 'us-east-1',
port: 3000,
});
const { url } = await proxy.listen();
console.log(`open ${url}`); // browser -> http://localhost:3000
// ... later
await proxy.close();Supplying tokens — three ways
Pick whichever fits; the first two keep the library pure transport (no AWS dependency):
// 1. static token (you manage refresh)
createProxy({ endpoint, token: '<jwe>' });
// 2. bring your own minter (SDK, your control plane, anything)
createProxy({ endpoint, getToken: async () => myMint() });
// 3. self-mint + auto-refresh via the aws CLI
createProxy({ endpoint, microvmId: 'microvm-…', region: 'us-east-1' });One MicroVM, many apps
A single MicroVM often runs several services on different in-VM ports (JupyterLab on 8888, TensorBoard on 6006, an API on 8080; or several agents). uvlink can front all of them from one process, two ways:
Mode A — listener-per-port (--map)
Each app gets its own local port. Nothing rewrites paths, redirects, or cookies, so even picky SPAs just work. The robust default.
npx uvlink --microvm-id microvm-… --region us-east-1 \
--map 3000:8888 \ # Jupyter -> http://localhost:3000
--map 3001:6006 \ # TensorBoard -> http://localhost:3001
--map 3002:8080 # API -> http://localhost:3002createProxy({
endpoint, microvmId: 'microvm-…', region: 'us-east-1',
routes: [
{ listen: 3000, backendPort: 8888 },
{ listen: 3001, backendPort: 6006 },
],
});Mode B — path-prefix on one origin (--route)
One local port; a path prefix selects the app. Great for fronting several
agents/services behind a single URL with no CORS between them (a control UI can
talk to every backend same-origin). The prefix is stripped before forwarding and
Location redirects are rewritten so apps mounted under a prefix still work.
npx uvlink --microvm-id microvm-… --region us-east-1 --port 3000 \
--route /agent-a=7001 \ # -> http://localhost:3000/agent-a
--route /agent-b=7002 \ # -> http://localhost:3000/agent-b
--route /console=7000 # -> http://localhost:3000/consolecreateProxy({
endpoint, microvmId: 'microvm-…', region: 'us-east-1', port: 3000,
routes: [
{ prefix: '/agent-a', backendPort: 7001, stripPrefix: true },
{ prefix: '/agent-b', backendPort: 7002, stripPrefix: true },
],
});Pass --no-strip-prefix (or stripPrefix: false) when the app is already mounted
under the prefix. Longest matching prefix wins; an unmatched path returns 404.
When self-minting, the token is automatically scoped to just the ports you
route to (least privilege) — pass --allowed-ports all (or allowedPorts: 'all')
to opt out, or a custom list to override. See examples/multi-app.mjs
(Mode A), examples/multi-agent.mjs (Mode B), and
docs/routing-spec.html for the design.
Share a MicroVM publicly (uvlink share)
Hand someone a link and they reach your MicroVM in a plain browser — no AWS credentials on their end — behind an access guard you control:
npx uvlink share --microvm-id microvm-… --region us-east-1 --expire 1h
#
# ┌─ uvlink share ───────────────────────────────────────────
# │ Send this link — anyone can open it, no AWS creds needed:
# │
# │ https://calm-forest-1234.trycloudflare.com/__uvlink/grant?t=…
# │
# │ Expires in ~60 min. Ctrl-C to revoke + stop sharing.
# └───────────────────────────────────────────────────────────What happens: uvlink starts a bridge, brings up a Cloudflare quick-tunnel
(needs cloudflared
on your PATH — no account required), and mints a signed, expiring grant link.
Opening the link sets a session cookie and drops the visitor into your app; every
later request (and WebSocket) is checked against that session before uvlink injects
the AWS token upstream. Ctrl-C revokes the link and stops sharing.
--expire 30m|2h|45s— link lifetime (default1h).--public-url <url>— bring your own public origin (skip the auto tunnel), e.g. when you already run your own tunnel or front uvlink with your own HTTPS.--sub <label>— label the grant for your own attribution/logs.- Works with the multi-app flags too (
--map/--route).
The access layer is createGuard() — an HMAC signed grant → session-cookie
guard (S3-presigned-style: stateless, fail-closed, mandatory expiry, revocable),
with a pluggable authorize(identity) seam for future identity/allowlist checks.
Laptop-first & honest scope. Your machine must stay awake while sharing — it is the validator and the tunnel's origin. The tunnel provider (Cloudflare) terminates the public TLS and can see request/response payloads in the clear; it never sees your AWS token (that rides uvlink's own leg to the VM).
This laptop + tunnel path is the recommended default for personal / hobby sharing: zero standing infrastructure, no inbound port opened on your machine, and it disappears the moment you
Ctrl-C. The guard — not the tunnel — is the security boundary, so a stranger who somehow finds the URL still gets401without a valid grant.Going beyond hobby? If you need an always-on URL, multiple concurrent recipients, or you can't route traffic through a third-party edge (sensitive / regulated data), stand up your own reverse proxy — run the same guard on infra you own so the laptop can close and no third party can read traffic. The ready-made path is
examples/t4g-deploy/(at4g.nano+ systemd + security group, ~$3/mo); pointuvlink share --public-url <your-origin>at it, or frontexamples/registry-server.mjswith your own HTTPS.
API
createProxy(options)
| option | type | notes |
|---|---|---|
| endpoint | string | required — MicroVM endpoint host (no scheme) |
| port | number | local port (default 3000) |
| host | string | bind address (default 127.0.0.1) |
| backendPort | string | in-VM port to route to (default 8080) |
| token | string | static token, or… |
| getToken | ()=>string\|Promise<string> | custom minter, or… |
| microvmId | string | self-mint via aws CLI (+ region, awsCli, ttlMinutes) |
| guard | object | a createGuard() instance — gates every request/upgrade (see uvlink share) |
| onLog | (level,msg)=>void | optional logger |
Returns { server, listen(), close() }.
createPool(options) — optional fan-out
Round-robin one browser-facing port across multiple MicroVM endpoints (each
with its own getToken). For real autoscaling/pooling/recycling, use a dedicated
load balancer; this is a convenience for simple fan-out.
import { createPool } from 'uvlink';
const pool = createPool({ targets: [
{ endpoint: 'a…on.aws', getToken: () => tokA },
{ endpoint: 'b…on.aws', getToken: () => tokB },
]});
await pool.listen(3000);How it works
browser ──HTTP/WS──▶ uvlink ──HTTPS──▶ MicroVM endpoint
· HTTP: add X-aws-proxy-auth + X-aws-proxy-port
· WS: prepend lambda-microvms.* subprotocols,
keep the app's own subprotocol
· token cached + refreshed before the 60-min TTL
· on a 403, re-mint the token and replay the request onceResilience & errors
- Token expiry is self-healing (HTTP). If the endpoint returns 403 because the token went stale (e.g. the MicroVM was idle past the token's life), uvlink refreshes the token and replays the request once. Bodies up to 1 MiB are buffered so the replay carries the same payload; larger/chunked bodies aren't retried.
- Errors say what went wrong. uvlink-generated failures carry an
x-uvlink-errorheader and auvlink [kind]: detailbody — e.g.upstream-unreachable(VM terminated or wrong endpoint),token-error,no-route. A pass-through 403 gets anx-uvlink-hintexplaining the token/port cause.
Examples
See examples/: JupyterLab, a static site, and a WebSocket app.
Caveats / honest scope
- This runs a local (or sidecar) proxy. It does not change the platform; it's the client-side bridge that works today. (A native browser-auth path would make it unnecessary — until then, this.)
- Self-minting shells out to the
awsCLI; for SDK-based or custom auth usegetToken. - For production multi-VM serving (pooling, scaling, health, recycling) you want a
real load balancer/autoscaler, not just
createPool.
Development
node test/run.mjs # runs against a local mock endpoint — no AWS neededLicense
MIT
