Files
captoys/diigo-dump/README.md
T
Connolly 3f8e77eaa2 feat: webapp DO factory worker; diigo-dump as its consumer
webapp/ (new, site-agnostic, independently deployable):
- WebApp: fluent client (pathjoin/query/withHeaders/cookies/get/post)
  as RpcTarget; flower-style unions cite NuxiNL/flower v0.11 (Jan 2019);
  url/headers private (RpcTarget property reads are RPC); Error messages
  carry no header/param values; query-stripped URLs only
- WebAppDo: per-instance authority in DO storage (survives eviction);
  one-shot configure(), refuses different-url re-configure
- WebAppFactory: newWebApp(init) -> {id, app}, at(id); ids are bearer
  capabilities and are not logged
- HTTP surface: GET / mint form (secret header keys render blank),
  POST / mint (form or JSON), POST /at/<id> drive-by-capability
- types via wrangler types (worker-configuration.d.ts, gitignored)

diigo-dump/: now a webapp consumer. The diigo knowledge is only the
mint call: url=interact_api/, UA + x-requested-with, cookie, 500ms gate.
- src/webapp.ts, src/diigo-web.ts deleted (webapp's are canonical);
  diigo.ts slimmed to dumpBookmarks + PagedGet
- /_connect is state-aware: connect form pre-mint; health check
  (count=1 probe) + re-mint path post-mint; home-page link relabeled
- mint success page: narrow field + copy button (attribute-quoting
  fixed; id in x-diigo-web-id header for scripts)
- scripts/diigo-login: askpass flow, marked OBSOLETE (page flow is
  the primary); speaks the form protocol; writes only DIIGO_WEB_ID —
  the cookie lives in DO storage, never on disk
- package.json: dev = wrangler dev -c wrangler.jsonc -c ../webapp/...
  (verified: Ready on :8787, GET / 200, /_connect 200);
  wrangler ^4.148.0 devDependency after node_modules was cleaned

TODO(webapp) in webapp/README.md: proxy-page vs primary-worker mint
form; reconsider as the UI grows.

Generated-by: opencode (GLM)
2026-10-06 23:30:27 -05:00

4.3 KiB

diigo-dump

Cloudflare Worker port of diigo-dump.mjs in the mm-diigo repo (projects/diigo-bak/diigo-dump.mjs) — dump Diigo bookmarks, most-recently-updated first, via the browser session cookie rather than the static API key.

What it does

One page: a page-number field and a dump button. Submitting POSTs to the worker, which fetches

https://www.diigo.com/interact_api/load_user_items
    ?sort=updated&count=24&page_num=<page>

with the session cookie and shows status, total, and the JSON items in a read-only <pre>. GET / is inert — dumps happen only on explicit submits.

Structure: WebApp as a Durable Object (webapp worker)

The credentialed authority is a Durable Object instance from the separately deployable ../webapp/ worker (service binding WEBAPP → its WebAppFactory entrypoint; diigo knowledge = only the mint call in src/index.ts). The instance is configured once — diigo's URL + seed headers (cookie, UA, x-requested-with) + 500ms rate gate — and persists its init in DO storage: it survives restarts, and diigo-dump or any other worker can reconnect purely by id. The cookie crosses the wire exactly once, at mint time.

Setup (dev):

cd diigo-dump
npx wrangler dev -c wrangler.jsonc -c ../webapp/wrangler.jsonc
# → http://localhost:8787 (webapp is secondary, binding-only)

npm run diigo-login >> .dev.vars   # askpass for the cookie, mints the
                                   # instance, writes DIIGO_COOKIE +
                                   # DIIGO_WEB_ID; then restart wrangler
# or, no askpass: open http://localhost:8787/_connect and paste the
# cookie into the mint form; it prints the DIIGO_WEB_ID line to add

Deployed:

cd ../webapp && npx wrangler deploy
cd ../diigo-dump && npx wrangler deploy
npx wrangler secret put DIIGO_COOKIE   # re-run diigo-login only when
npx wrangler secret put DIIGO_WEB_ID   # the cookie itself expires

Re-login is needed only when diigo invalidates the cookie; re-minting (POST /_mint with the new cookie) yields a fresh id and no other changes.

Provenance notes: the user-agent / x-requested-with headers and the interact_api endpoint mirror the original script; the fluent client from WebApp.js is restored in src/webapp.ts with WebApp and the rate-limit wrapper as RpcTarget subclasses. src/diigo.ts holds makeDiigoAPI and the dumpBookmarks generator; DiigoService.open() hands an RPC caller a rate-limited stub with the cookie attached only on the server side, and promise pipelining makes a whole pathjoin().query().get() chain a single round trip. One deliberate divergence from the original: url is a private field, not a public property — on an RpcTarget, a public property is readable over RPC (a property read is an argument-less method call), and a query string can carry credentials, so a public url would leak them to any stub holder.

RPC use (from another worker, same account)

// wrangler.jsonc of the caller
"services": [{ "binding": "DIIGO", "service": "diigo-dump", "entrypoint": "DiigoService" }]
import { dumpBookmarks } from "./diigo.js";

const stub = env.DIIGO.open(); // rate-limited, cookie stays server-side
// pipeline: one round trip for the whole chain
const first = JSON.parse(await stub.pathjoin("load_user_items")
	.query({ sort: "updated", count: 24, page_num: 1 }).get());
// or run the whole dump in this worker's address space:
for await (const item of dumpBookmarks({ diigoAPI: stub, pages: 2 })) {
	console.log(item);
}

Configuration

  • Secret DIIGO_COOKIE: the Cookie header from diigo.com devtools.
    • Local dev: .dev.vars (gitignored). Note wrangler reads it only at startup — restart after edits.
    • Deployed: npx wrangler secret put DIIGO_COOKIE.

Run

cd diigo-dump
npx wrangler dev -c wrangler.jsonc -c ../webapp/wrangler.jsonc
# → http://localhost:8787; webapp rides along, binding-only
# (first run: npm run diigo-login >> .dev.vars, or the /_connect page;
# restart wrangler after adding DIIGO_WEB_ID)

Status as of 2026-10-06: manual integration test passes — 200, total: 1674. See the project ladder in ../capsite-checklist.md; context in ../capsite-notes.md. This is the cookie path; the API-key path (EmDash plugin) is still 401.