capsite-notes: What-we-built gains the webapp/diigo-dump layers (links to READMEs and the goal tree, not repeats); root Goal restated as 'confine the diigo secret to a small auditable piece behind an attenuated interface' per discussion, with the dead API-key path noted and details delegated to the tree; credentials section de-staled (placeholder values, unconditional 401 finding, cookie-path direction); private-bookmarks note no longer recommends the dead HTTP Basic path. diigo-bookmarks/README: title corrected sandboxed->native with the conversion note and a disclaimer that its Develop/Publish sections are inherited scaffold text (larger rework deferred). Generated-by: opencode (GLM)
5.7 KiB
madmode.com follows the "bake, don't fry" static site generator pattern, which avoids all sorts of security issues.
For years (#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 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 for the current credential story). If it works out well, we should be able to expand to bib sync #195.
What we built
webapp/— site-agnostic Durable-Object factory for attenuated credentialed web clients (fluent client, ids are bearer capabilities); webapp/README.mddiigo-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 for the verified/not-yet split)madmode/— Cloudflare Workers EmDash site (Starter template), local dev viapnpm dev.diigo-bookmarks/— native EmDash plugin (converted from sandboxed):src/index.tsexportscreatePlugin()which returnsdefinePlugin({...}).- Declares
capabilities: ["network:request"],allowedHosts: ["secure.diigo.com", "www.diigo.com"](Diigo 301ssecure.diigo.com→www.diigo.com; the host gate re-validates every redirect hop). - Fetches
https://www.diigo.com/api/v2/bookmarkswith HTTP Basic auth into plugin storage, serves a public JSON route, and has a Block Kit admin page ("Sync now" button + table).
- Site
/bookmarkspage 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:
// 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:
PluginContexthas noenv. Worker secrets are read via dynamicimport("virtual:emdash/env")(generated module re-exportscloudflare:workersenvunder @astrojs/cloudflare;undefinedon other adapters).- GET plugin routes get query params in
ctx.input(aPluginRouteQueryrecord), notctx.queryorctx.request.urlparsing. - 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 theX-EmDash-Request: 1header (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'sadminPagesis NOT merged into the manifest; the admin sidebar readsplugin.admin.pagesfrom the entrypoint's ResolvedPlugin. Withadmin.pagesset, the manifest reportsadminMode: "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 aplugin:activaterun by default).- Local
.pnpmhardlinks mean source edits reach the site only after a full dev-server restart; if you delete a stalenode_modules/.vite/deps_ssr/*.jsby hand the server 500s untilrm -rf node_modules/.vite+ full restart.
Where the Diigo credentials go
The credential story moved twice; the goal tree 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 <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
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
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).