docs: notes layering + goal reduced to confinement principle
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)
This commit is contained in:
1 parent
fcb6ab9b0e
commit
2c7d8c9b4d
2 files changed
+159
No files matched your search
@@ -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: "<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 (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
|
||||||
|
|
||||||
|
```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).
|
||||||
@@ -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.
|
||||||
Reference in new issue
Block a user