Some sites check a double-submit token: the session value sent as a cookie and again in the form body. postForm(fields, echoCookies) builds a multipart body and copies the named cookies (from the header store, at send time) into fields, so the caller never sees the value. It returns a Reply instead of throwing on non-OK: status, content type, content-disposition, the redirect location with its query stripped, and the body as bytes (the caller decodes, e.g. per a charset the body declares). Redirects are not followed, so an expired session shows up as a 3xx. Set-Cookie is not passed back. Errors name keys, never values. Generated-by: Claude Opus 5.5
70 lines
4.2 KiB
Markdown
70 lines
4.2 KiB
Markdown
# webapp
|
|
|
|
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-cache CONTRIBUTING](../diigo-cache/CONTRIBUTING.md)) 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=…&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, storeId}` (and `x-webapp-id`, `x-webapp-store-id`). Neither id is 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).
|
|
- `PUT /store/<storeId>/<key>` — set a header-store entry (JSON value body), e.g. `PUT /store/<storeId>/cookie` with `"session=…"` rotates the cookie; `DELETE` removes it. → `{ok: true}` (or `{error}`; messages carry no keys or values).
|
|
|
|
## RPC surface (same-account workers; binding `entrypoint: "WebAppFactory"`)
|
|
|
|
- `fetch(request)` — the HTTP surface above, over the binding
|
|
- `newWebApp({url, headers?, msPerCall?})` → `{id, storeId, app}` — the fluent stub;
|
|
promise pipelining makes a whole `pathjoin().query().get()` chain a
|
|
single round trip
|
|
- `at(id)` — the fluent stub for a previously-minted instance
|
|
- on a stub, `postForm(fields?, echoCookies?)` → `{status, contentType,
|
|
contentDisposition?, location?, body}`: a multipart/form-data POST.
|
|
`echoCookies` maps a form field to a cookie name; the cookie's value
|
|
(header store included) fills the field at send, so a double-submit
|
|
CSRF token goes out without the holder seeing it. Redirects are not
|
|
followed and a non-OK status is a reply, not an Error (a 3xx to a login
|
|
page is how many sites say "session expired"). `location` comes back
|
|
query-stripped; `set-cookie` never comes back. `body` is bytes: the
|
|
caller knows its charset.
|
|
- `store(storeId)` — a header store, with full authority:
|
|
`set("cookie", …)` rotates the cookie under every handle already out
|
|
|
|
## The header store: two ids, two authorities
|
|
|
|
Mint makes two things: the instance, and a header store (a `MapStoreDo`
|
|
from [the capsite lib](../lib/src/store-do.ts), shaped
|
|
`("cookie", { keyShape: "cookie", valueShape: M.string() })`). A `cookie`
|
|
header goes into the store, not the instance's `init`; the instance reads
|
|
the store's read-only view at every send.
|
|
|
|
- the **instance id** is authority to *use* the cookie: it goes to the
|
|
cache's wiring.
|
|
- the **store id** is authority to *choose* it: it stays with the account
|
|
owner, who can rotate it (log in again, `PUT`) or revoke it (`DELETE`,
|
|
or log out).
|
|
|
|
Kept apart because whoever can choose the cookie can swap in another
|
|
account's session: if the cache's id could rotate, a leaked instance id
|
|
would let Mallory fill the cache (and its public page) with her own
|
|
bookmarks, and the owner's logout would not stop it.
|
|
|
|
Instances minted before header stores keep their cookie in
|
|
`init.headers`, unchanged; they work as before and cannot rotate.
|
|
|
|
## Run
|
|
|
|
pnpm test # header store + rotation (vitest, workerd pool)
|
|
npx wrangler dev # → its own ready line (usually :8787 with the
|
|
# two-config diigo-cache run; standalone, :8788)
|
|
|
|
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.
|