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:
1 parent
58be2ccaf8
commit
cbf1435571
2 files changed
+64
-18
No files matched your search
+10
-7
@@ -3,19 +3,21 @@
|
||||
DO-backed fluent web client factory: site-agnostic. Deploy once; instances
|
||||
are attenuated handles whose **ids are bearer capabilities** — anyone
|
||||
holding the id string can drive the instance; nobody without it can.
|
||||
TODO: this proxy-page flow (see diigo-dump README) vs running webapp as
|
||||
the primary worker (`wrangler dev -c ../webapp/wrangler.jsonc -c ../diigo-dump/wrangler.jsonc`)
|
||||
TODO: this proxy-page flow (see diigo-cache README) vs running webapp as
|
||||
the primary worker (`wrangler dev -c ../webapp/wrangler.jsonc -c ../diigo-cache/wrangler.jsonc`)
|
||||
so its own port serves the mint form directly; reconsider when the UI
|
||||
grows past a form or two.
|
||||
|
||||
## HTTP surface
|
||||
|
||||
- `GET /` — the mint form. Prefill via query string: `?url=…&headers=<urlencoded JSON>&msPerCall=…`; secret header keys (cookie, authorization, proxy-authorization) render **blank** — paste them at mint time; never echoed back.
|
||||
- `GET /` — the mint form. Prefill via query string: `?url=…&headers=<urlencoded JSON>&msPerCall=…&title=…`; secret header keys (cookie, authorization, proxy-authorization) render **blank** — paste them at mint time; never echoed back. A dedicated `cookie` field is sugar for the `cookie` header (if the JSON also has one, they must agree).
|
||||
- `GET /form?render=1&…` — the same form for a proxying worker (diigo-cache's `/_connect`): the action is left as an `ACTION` placeholder for the proxy to point at its own relay.
|
||||
- `POST /` — mint. Form-encoded (that page) or JSON `{url, headers, msPerCall}` (service-style). Responds `{id}` (and `x-webapp-id`). The id is *not* logged: possession is authority.
|
||||
- `POST /at/<id>` — drive an instance by capability: JSON `{path?, query?, method?, body?}` → the fluent chain → `{status, text}` (or `{error}`; messages carry no header/param values).
|
||||
|
||||
## RPC surface (same-account workers; binding `entrypoint: "WebAppFactory"`)
|
||||
|
||||
- `fetch(request)` — the HTTP surface above, over the binding
|
||||
- `newWebApp({url, headers?, msPerCall?})` → `{id, app}` — the fluent stub;
|
||||
promise pipelining makes a whole `pathjoin().query().get()` chain a
|
||||
single round trip
|
||||
@@ -24,8 +26,9 @@ grows past a form or two.
|
||||
## Run
|
||||
|
||||
npx wrangler dev # → its own ready line (usually :8787 with the
|
||||
# two-config diigo-dump run; standalone, :8788)
|
||||
# two-config diigo-cache run; standalone, :8788)
|
||||
|
||||
Status: typechecks; mint + drive not yet exercised end-to-end (needs the
|
||||
combined dev run; see ../capsite-checklist.md — it is the single
|
||||
source for run/connect/deploy instructions).
|
||||
Status: mint + drive exercised end-to-end in the combined dev run
|
||||
(through diigo-cache's create flow, 2026-10-07); not yet deployed. See
|
||||
../capsite-checklist.md — the single source for run/connect/deploy
|
||||
instructions.
|
||||
+54
-11
@@ -211,17 +211,18 @@ export class WebAppDo extends DurableObject<{ WEBAPP?: never }> {
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
/** 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> {
|
||||
): 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);
|
||||
return mintFormPage(url.searchParams);
|
||||
}
|
||||
|
||||
// mint: browser form POST (form-encoded) or JSON (service-style)
|
||||
@@ -229,6 +230,14 @@ export default {
|
||||
return mint(request, env.WEBAPP_DO);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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-]+)$/);
|
||||
@@ -238,12 +247,17 @@ export default {
|
||||
}
|
||||
|
||||
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 = {
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user