Files
captoys/capsite-notes.md
T
dckc d857018e4b fixup! docs: diigo-cache UX map
The public page's write access is resolved: it reads through viewer().

Generated-by: Claude Opus 5.5
2026-10-07 09:26:09 -05:00

165 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
`madmode.com` follows the "bake, don't fry" static site generator pattern, which avoids all sorts of security issues.
For years ([#23](https://github.com/dckc/madmode-blog/issues/23) was created in 2016) I have been wishing for a way to exercise read capabilities to get things like bookmarks as well as write access to put it on my site.
Let's give this a try:
- [EmDash 1.0: the stable CMS with a secure plugin registry | Cloudflare Blog](https://blog.cloudflare.com/emdash-cms-plugin-registry/) September 28, 2026
Goal: confine the code that has access to the diigo.com secret to a small,
auditable piece behind an attenuated interface (originally "the API key held
by the host, never inside the plugin"; the API-key path turned out dead —
see [the goal tree](capsite-checklist.md) for the current credential story).
If it works out well, we should be able to expand to [bib sync #195](https://github.com/dckc/madmode-blog/issues/195).
## What we built
- `webapp/` — site-agnostic Durable-Object factory for attenuated credentialed
web clients (fluent client, ids are bearer capabilities);
[webapp/README.md](webapp/README.md)
- `diigo-cache/` — the webapp consumer, evolved from the diigo-dump worker:
bookmark dumps cached in DO SQLite storage (private cache by unique id,
public-only cache by username-in-path); the mint call carries all diigo
knowledge; the cookie lives in the DO's storage (see
[the checklist's goal tree](capsite-checklist.md) for the verified/not-yet
split)
- `madmode/` — Cloudflare Workers EmDash site (Starter template), local dev via `pnpm dev`.
- `diigo-bookmarks/` — **native** EmDash plugin (converted from sandboxed):
- `src/index.ts` exports `createPlugin()` which returns `definePlugin({...})`.
- Declares `capabilities: ["network:request"]`, `allowedHosts: ["secure.diigo.com", "www.diigo.com"]`
(Diigo 301s `secure.diigo.com` → `www.diigo.com`; the host gate re-validates every redirect hop).
- Fetches `https://www.diigo.com/api/v2/bookmarks` with HTTP Basic auth into plugin storage,
serves a public JSON route, and has a Block Kit admin page ("Sync now" button + table).
- Site `/bookmarks` page renders the collected bookmarks (linked from the primary menu).
## diigo-cache UX map (2026-10-07)
What a person can do, where, and with what authority. Input to aligning
the admin UX with the RPC interface (checklist: the store refactor and
"align admin UX and RPC").
```mermaid
flowchart LR
home["/ home<br/>(no authority)"]
connect["/_connect<br/>mint form"]
wire["wire step<br/>(webapp id)"]
created["cache created<br/>(cache id)"]
login["/cache/ login<br/>(password manager)"]
admin["webkey page<br/>/cache/ + key in fragment"]
public["/public/username<br/>(anyone)"]
home -->|create| connect
connect -->|"POST /_mint (cookie)"| wire
home -->|"POST /_wire (webapp id)"| created
wire -->|"POST /_wire"| created
created -->|"POST /_publish (cache id)"| created
created -->|"open this cache"| admin
home -->|"open a cache"| login
login -->|"cache id"| admin
home -->|"dump: POST / (cache id + page)"| home
admin -->|"api.cache(key) …"| admin
created -->|"link (public mode)"| public
```
| action | where | transport | authority |
|---|---|---|---|
| mint the diigo session | `/_connect` | HTTP `POST /_mint` (relays webapp's form) | the cookie |
| wire a cache (private/public) | wire step, home | HTTP `POST /_wire` | webapp id |
| save the ids | created page | browser (copy, password manager) | — |
| publish later | created page | HTTP `POST /_publish` ⚠ | cache id |
| dump a page (raw rows) | home | HTTP `POST /` ⚠ | cache id |
| fingerprint, has-public | webkey page | RPC `fingerprint()`, `hasPublic()` | cache key |
| fill / stop / status | webkey page | RPC `startFill()`, `stopFill()`, `fillStatus()` | cache key |
| …of the public companion | webkey page | RPC `publicCache().…` | cache key |
| invalidate the last N days | webkey page | RPC `invalidateRecent(days)` | cache key |
| read public bookmarks | `/public/<u>` | HTTP GET (HTML or JSON) ⚠ | none |
| bulk dump for another worker | — | service binding `DiigoBridge.dumpNdjson` | worker config |
⚠ misalignments:
- publish and dump are admin actions held by the cache key, but live on
HTTP form routes rather than the RPC facet the webkey page uses
- ~~the public page can write~~ — resolved: it reads only through the
read-only `viewer()` facet; unfetched pages say so
- the webkey page has no dump or publish; the home page has no fill
## Native-plugin wiring (the part that took a while)
`emdash()`'s `plugins: []` takes **descriptors**, not plugin objects — the Astro
integration needs a module specifier it can statically import at build time:
```js
// astro.config.mjs
plugins: [{ id: "diigo-bookmarks", version: "0.1.0", entrypoint: "diigo-bookmarks" }]
```
The generated runtime module does `import { createPlugin } from "diigo-bookmarks"`
and calls it. `definePlugin(...)` results have no `entrypoint` and are rejected
with a build-time error. An in-config factory call also fails ("has no entrypoint").
Key facts verified from emdash source:
- `PluginContext` has **no `env`**. Worker secrets are read via dynamic
`import("virtual:emdash/env")` (generated module re-exports `cloudflare:workers`
`env` under @astrojs/cloudflare; `undefined` on other adapters).
- GET plugin routes get query params in `ctx.input` (a `PluginRouteQuery` record),
not `ctx.query` or `ctx.request.url` parsing.
- Admin (Block Kit) route: the admin frontend POSTs JSON
`{ type: "page_load", page: "<declared page path>" }` and button clicks as
`{ type: "block_action", action_id, page }` to `/_emdash/api/plugins/<id>/admin`;
the handler returns `{ blocks, toast? }`. CSRF requires the `X-EmDash-Request: 1`
header (the frontend sends it; curl must add it explicitly).
- Admin pages/widgets for **native** plugins must be declared in the plugin's own
`definePlugin({ admin: { pages: [...] } })` — the build descriptor's
`adminPages` is NOT merged into the manifest; the admin sidebar reads
`plugin.admin.pages` from the entrypoint's ResolvedPlugin. With `admin.pages`
set, the manifest reports `adminMode: "blocks"` and the sidebar gains
Plugins → Diigo Sync at `/_emdash/admin/plugins/diigo-bookmarks/sync`.
- `ctx.cron.schedule()` upserts, so calling it on every sync is a safe bootstrap
for configured plugins (which don't get a `plugin:activate` run by default).
- Local `.pnpm` hardlinks mean source edits reach the site only after a full
dev-server restart; if you delete a stale `node_modules/.vite/deps_ssr/*.js`
by hand the server 500s until `rm -rf node_modules/.vite` + full restart.
## Where the Diigo credentials go
The credential story moved twice; [the goal tree](capsite-checklist.md) is
authoritative. Current shape: the session cookie lives only in the webapp
DO's storage; `madmode/.dev.vars` still holds placeholder
`DIIGO_API_KEY` / `DIIGO_USERNAME` for the (dead) API-key path — both
documented schemes return 401 unconditionally, verified against both known
real keys (opencode session ses_eec320fe4ffehaMAn44U1nTKJU: `key=` param,
`key`+`user=`, and HTTP Basic, on both /api/v2/bookmarks and /api/v2/notes,
though the same creds downloaded 13.5k bookmarks in 2024–2025; diigo
evidently revoked v2 key auth). Deployed secrets: `cd madmode && pnpm
wrangler secret put <NAME>`; the sync rides the cookie path since the API
path died.
`EMDASH_ENCRYPTION_KEY` (in `madmode/.env` locally) is only needed for sandboxed
plugins' secret settings; the native plugin doesn't use it, but keep it backed up
for the site.
## How to run it
```bash
cd madmode
pnpm dev # → http://localhost:4321, admin at /_emdash/admin
# one-time auth helper (skips passkey):
# http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
```
- Admin page: `/_emdash/admin` → plugins → Diigo Sync → "Sync now"
- Public JSON: `/_emdash/api/plugins/diigo-bookmarks/bookmarks?limit=20`
- Site page: `/bookmarks`
- Sync also self-schedules via cron (`0 */6 * * *`), stored in `_emdash_cron_tasks`.
Note: private Diigo bookmarks need the session-cookie channel (which is what
the sync will use after goal 2; the API-key path is dead).
## Plugin dev loop
```bash
cd diigo-bookmarks
pnpm typecheck # tsc --noEmit
```
Then restart `pnpm dev` in `madmode/` (full stop + start; config/plugin changes
are not hot-reloaded through the .pnpm hardlink path).