webapp: mint form servable by a proxying worker

wrangler's multi-config dev binds aux workers but doesn't serve their
HTTP, so diigo-cache relays webapp's own mint form:

- WebAppFactory gains fetch(): the HTTP surface over the binding
- GET /form?render=1 leaves the form action as a placeholder the proxy
  points at its relay; ?title= sets the page title
- a dedicated cookie field (sugar for the cookie header; must agree
  with the JSON if both are given)

Generated-by: opencode (GLM)
This commit is contained in:
dckc committed 2026-10-07 04:19:54 -05:00
1 parent 58be2ccaf8
commit cbf1435571
2 files changed
+86 -40

No files matched your search

+76 -33
View File
@@ -211,39 +211,53 @@ export class WebAppDo extends DurableObject<{ WEBAPP?: never }> {
}
}
export default {
async fetch(
request: Request,
env: { WEBAPP_DO: DurableObjectNamespace<WebAppDo> },
): Promise<Response> {
const url = new URL(request.url);
/** the worker's whole HTTP surface (the default export and the
* WebAppFactory entrypoint share it) */
const defaultFetch = async (
request: Request,
env: { WEBAPP_DO: DurableObjectNamespace<WebAppDo> },
): Promise<Response> => {
const url = new URL(request.url);
// mint form: site-agnostic; secret headers (cookie, authorization) are
// not prefilled from the query string — the form leaves them blank
if (url.pathname === "/" && request.method === "GET") {
return mintForm(url.searchParams);
}
// mint form: site-agnostic; secret headers (cookie, authorization) are
// not prefilled from the query string — the form leaves them blank
if (url.pathname === "/" && request.method === "GET") {
return mintFormPage(url.searchParams);
}
// mint: browser form POST (form-encoded) or JSON (service-style)
if (url.pathname === "/" && request.method === "POST") {
return mint(request, env.WEBAPP_DO);
}
// mint: browser form POST (form-encoded) or JSON (service-style)
if (url.pathname === "/" && request.method === "POST") {
return mint(request, env.WEBAPP_DO);
}
// the id is the capability: dereference it over plain HTTP.
// POST /at/<id> with JSON {path?, query?, method?, body?}
const at = url.pathname.match(/^\/at\/([A-Za-z0-9-]+)$/);
if (at && request.method === "POST") {
const app = env.WEBAPP_DO.get(env.WEBAPP_DO.idFromString(at[1])).handle();
return drive(await app, request);
}
// the mint form, renderable for a proxying worker: ?render=1 keeps the
// HTML identical but leaves the form's action at a relative `ACTION`
// placeholder — the proxying worker substitutes its own relay path.
// (Secret header keys still render blank.)
if (url.pathname === "/form" && request.method === "GET") {
return mintFormPage(url.searchParams);
}
return new Response("not found", { status: 404 });
},
// the id is the capability: dereference it over plain HTTP.
// POST /at/<id> with JSON {path?, query?, method?, body?}
const at = url.pathname.match(/^\/at\/([A-Za-z0-9-]+)$/);
if (at && request.method === "POST") {
const app = env.WEBAPP_DO.get(env.WEBAPP_DO.idFromString(at[1])).handle();
return drive(await app, request);
}
return new Response("not found", { status: 404 });
};
function mintForm(params: URLSearchParams): Response {
export default { fetch: defaultFetch };
/** GET / mint form, or (via /form) the same HTML with the action left as
* a placeholder for a proxying worker to substitute */
function mintFormPage(params: URLSearchParams): Response {
const render = params.get("render") === "1";
const url = params.get("url") ?? "";
const msPerCall = params.get("msPerCall") ?? "500";
const title = params.get("title") ?? "webapp: mint an instance";
// headers come prefilled as JSON; secret keys are blanked for the form
let headersJson = "";
try {
@@ -258,14 +272,18 @@ function mintForm(params: URLSearchParams): Response {
} catch {
// ignore malformed prefill
}
const action = render ? "ACTION" : "/";
const body = `<!doctype html>
<html><head><meta charset="utf-8"><title>webapp: mint an instance</title></head>
<html><head><meta charset="utf-8"><title>${esc(title)}</title></head>
<body>
<h1>mint a webapp instance</h1>
<form method="post" action="/">
${render ? "<p>served by the referring worker; fields are webapp's init params (the action points back there)</p>" : ""}
<form method="post" action="${action}">
<p><label>base URL <input name="url" size="60" value="${esc(url)}"></label></p>
<p><label>headers (JSON; secret values stay server-side, leave blank to keep blank):
<textarea name="headers" rows="6" cols="60">${esc(headersJson)}</textarea></label></p>
<p><label>cookie <input type="password" name="cookie" size="80" autocomplete="off"></label>
<em>— the common secret header, sugar for adding <code>"cookie"</code> to the JSON yourself; if both are set they must agree</em></p>
<p><label>min ms between calls <input name="msPerCall" type="number" value="${esc(msPerCall)}"></label></p>
<p><button type="submit">mint</button></p>
</form>
@@ -275,6 +293,17 @@ function mintForm(params: URLSearchParams): Response {
const SECRET_KEYS = new Set(["cookie", "authorization", "proxy-authorization"]);
/** merge the form's dedicated `cookie` field into the headers (the sugar
* input; if the JSON also carries `cookie`, both values must agree) */
function withCookie(init: WebAppInit, cookie: string): WebAppInit {
if (!cookie) return init;
if (init.headers?.cookie && init.headers.cookie !== cookie) {
// key name only: values must not appear in error messages
throw new Error("cookie: contradicts the JSON headers");
}
return { ...init, headers: { ...(init.headers ?? {}), cookie } };
}
/** mint a fresh instance from form-encoded or JSON input */
async function mint(
request: Request,
@@ -287,11 +316,14 @@ async function mint(
init = (await request.json()) as WebAppInit;
} else {
const form = await request.formData();
init = {
url: String(form.get("url") ?? ""),
headers: JSON.parse(String(form.get("headers") || "{}")) as Record<string, string>,
msPerCall: Number(form.get("msPerCall") ?? 0),
};
init = withCookie(
{
url: String(form.get("url") ?? ""),
headers: JSON.parse(String(form.get("headers") || "{}")) as Record<string, string>,
msPerCall: Number(form.get("msPerCall") ?? 0),
},
String(form.get("cookie") || ""),
);
}
} catch (err) {
return Response.json({ error: `bad input: ${String(err)}` }, { status: 400 });
@@ -374,8 +406,19 @@ function unionQuery(base: string, params: Query): string {
* const app2 = env.WEBAPP.at(id);
*/
export class WebAppFactory extends WorkerEntrypoint {
/** the same HTTP surface as the default export, over the entrypoint
* (for service-binding callers that proxy pages, e.g. diigo-cache's
* /_connect → this form) */
async fetch(request: Request): Promise<Response> {
const env = this.env as { WEBAPP_DO: DurableObjectNamespace<WebAppDo> };
return defaultFetch(request, env);
}
/** mint a fresh instance: unforgeable uniqueId + configured fluent stub.
* The caller decides who else ever learns the id. */
* The caller decides who else ever learns the id. (If the caller only
* wanted the id, it must dispose() the app stub — workerd warns about
* undisposed stubs; see the `at()` alternative + `/at/<id>` route for
* id-only flows.) */
async newWebApp(init: WebAppInit): Promise<{ id: string; app: FluentGet }> {
new URL(init.url); // fail before minting on garbage
const ns = this.env.WEBAPP_DO;