Files
captoys/webapp/README.md
dckc ff2e7841f2 capsite/webapp: postForm, a multipart POST that can echo a cookie into a field
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
2026-10-11 14:22:09 -05:00

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.