diff --git a/capsite-notes.md b/capsite-notes.md new file mode 100644 index 0000000..18e28f9 --- /dev/null +++ b/capsite-notes.md @@ -0,0 +1,110 @@ +`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-dump/` — the webapp consumer: its 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). + +## 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: "" }` and button clicks as + `{ type: "block_action", action_id, page }` to `/_emdash/api/plugins//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 (see checklist goal 2). Deployed secrets: `cd madmode && pnpm +wrangler secret put `; 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). \ No newline at end of file diff --git a/diigo-bookmarks/README.md b/diigo-bookmarks/README.md new file mode 100644 index 0000000..3f3645a --- /dev/null +++ b/diigo-bookmarks/README.md @@ -0,0 +1,49 @@ +# diigo-bookmarks + +A **native** plugin for [EmDash CMS](https://emdashcms.com) (converted from +the sandboxed scaffold; wired via the `plugins: []` descriptor list in the +site's astro.config — see [the capsite notes](../capsite-notes.md)). + +Note: this README's Develop/Publish sections below are inherited from the +sandboxed scaffold and do not match the native wiring. + +## Develop + +```sh +pnpm install +pnpm run validate +pnpm run typecheck +pnpm run test +pnpm run build +``` + +To test against a running EmDash site, run `pnpm run dev` in this +directory (rebuilds on save) and `pnpm add file:../path/to/this` +in the site. Then `import diigoBookmarks from "diigo-bookmarks"` and pass +it into `emdash({ sandboxed: [diigoBookmarks] })`. + +`pnpm run test` builds the plugin and runs its tests through Worker Loader using +EmDash's production sandbox wrapper and host bridge. + +## Publish + +```sh +pnpm run login -- alice.example.com +pnpm run publish # builds and uploads artifacts to your PDS +``` + +To publish from GitHub Actions, run `pnpm run release:setup`. The command +creates one shared workflow at the Git repository root. + +## Version bumps + +Bump `version` in `package.json` when you ship a release. The +scaffold's `emdash-plugin.jsonc` deliberately omits `version` — +the build pipeline reads it from `package.json` so there's a single +source of truth. **Bump major** for breaking changes, **bump minor** +for new routes or hooks, **bump patch** for fixes. + +You MUST bump version whenever you change `capabilities`, `allowedHosts`, +or `storage` in the manifest. Installed users have consented to the +old trust contract; a change without a version bump would let new +behaviour slip past consent.