commit 126aab5cbead70ef066bbe5cbbbf4442460782d0 Author: Connolly Date: Tue Oct 6 21:33:25 2026 -0500 feat(diigo-dump): session-cookie dump worker, manual test passing Port of mm-diigo/projects/diigo-bak/diigo-dump.mjs as a Cloudflare Worker: one page-number field + button, GET interact_api/load_user_items (sort=updated, count=24) with DIIGO_COOKIE as a Worker secret. GET / is inert; dumps happen only on POST submit. Manual integration testing (local wrangler dev, port 8788, 2026-10-06) shows it works: status 200, total 1674 bookmarks, page-1 items returned. The browser-session-cookie path authenticates; the static API-key path (diigo-ping worker, EmDash plugin) is still rejected by Diigo (401). Also: project checklist (capsite-checklist.md) with step results, and a .gitignore for local artifacts (.dev.vars, node_modules, wrangler state). Generated-by: opencode (GLM) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a66d8eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# local working artifacts +.node_modules +node_modules/ +.dev.vars +.env +.dev.vars* +dist/ +.astro/ +.wrangler/ +worker-configuration.d.ts +emdash-env.d.ts \ No newline at end of file diff --git a/capsite-checklist.md b/capsite-checklist.md new file mode 100644 index 0000000..a83bf6d --- /dev/null +++ b/capsite-checklist.md @@ -0,0 +1,60 @@ +# capsite checklist + +Goal ladder toward "EmDash site collecting diigo.com bookmarks with the API key +held by the host, never inside the plugin" (see capsite-notes.md). +Start small; each step must pass before moving down. + +- [x] **1. Plain worker ping** (`diigo-ping/`): a Cloudflare Worker with one + ping button that calls `https://www.diigo.com/api/v2/bookmarks` with HTTP + Basic auth (`username:apiKey`) and shows the raw result in a `
`.
+      Secrets from `.dev.vars` locally / `wrangler secret put` deployed.
+      **Win:** page shows `status: 200 ok` and real bookmark JSON.
+      **Result (2026-10-06):** worker runs; Diigo returns 401 with the current
+      key/username pair — API-key path not yet authenticated.
+- [x] **1b. Cookie-path dump worker** (`diigo-dump/`): port of
+      `mm-diigo/projects/diigo-bak/diigo-dump.mjs` — one page-number field +
+      button, GET `interact_api/load_user_items?sort=updated&count=24&page_num=N`
+      with the browser session cookie (`DIIGO_COOKIE` secret).
+      **Result (2026-10-06):** manual integration test PASSES —
+      status 200, `total: 1674`, page-1 items returned. GET / is inert; dump
+      only on POST submit. The session-cookie / interact_api path works while
+      the API-key path is rejected.
+- [ ] **2. Real credentials in madmode/.dev.vars**: replace the placeholder
+      `DIIGO_API_KEY="dckc-madmode"` with a key from
+      https://www.diigo.com/settings/api; confirm the EmDash plugin's
+      "Sync now" returns success (status 200, bookmarks stored).
+- [ ] **3. `/bookmarks` page renders real data** after a sync — titles/tags
+      visible at `http://localhost:4321/bookmarks`.
+- [ ] **4. Cron sync works** locally without errors (`0 */6 * * *` task in
+      `_emdash_cron_tasks`).
+- [ ] **5. Deploy**: madmode to Workers `pnpm wrangler deploy`, secrets via
+      `wrangler secret put`; ping page then plugin "Sync now" against prod.
+- [ ] **6. bib sync #195**: repeat the pattern for the next read-access need.
+
+## Step 1 how-to: diigo-ping
+
+```bash
+cd diigo-ping
+printf 'DIIGO_API_KEY="dckc-madmode"\nDIIGO_USERNAME="6edadc55c0329434"\n' > .dev.vars
+npx wrangler dev            # → http://localhost:8787
+```
+
+Click the button; expected right now (same credentials as the plugin):
+Diigo `401 ... "HTTP Basic: Access denied"`. If even the standalone worker
+can't authenticate, the credentials are wrong, not the plugin wiring.
+
+Note: Diigo 301s `secure.diigo.com` → `www.diigo.com`; fetch follows the
+redirect, and the Authorization header survives host-to-host here — if Diigo
+ever strips it, switch the URL to `www.diigo.com` explicitly.
+
+## Step 1b how-to: diigo-dump (session-cookie path)
+
+```bash
+cd diigo-dump
+# .dev.vars: DIIGO_COOKIE=""
+npx wrangler dev --port 8788   # → http://localhost:8788
+wrangler restarts pick up .dev.vars edits only at startup
+```
+
+Enter a page number, click dump → status + `total` + JSON items. GET `/` is
+inert (no accidental fetches); dumps happen only on POST submits.
\ No newline at end of file
diff --git a/diigo-dump/src/index.ts b/diigo-dump/src/index.ts
new file mode 100644
index 0000000..7aadc65
--- /dev/null
+++ b/diigo-dump/src/index.ts
@@ -0,0 +1,90 @@
+// diigo-dump as a worker: one page-number field + button.
+// Port of mm-diigo/projects/diigo-bak/diigo-dump.mjs —
+// browser-session-cookie access to diigo's interact_api/load_user_items
+// (the API-key path is bkmkeep.py / the EmDash plugin; this one is the cookie path).
+// Secret: DIIGO_COOKIE (copy the "Cookie" header from diigo.com devtools).
+
+const DIIGO_BASE = "https://www.diigo.com/interact_api/";
+const COUNT = 24;
+
+type Env = { DIIGO_COOKIE?: string };
+
+export default {
+	async fetch(request: Request, env: Env): Promise {
+		const url = new URL(request.url);
+		if (url.pathname !== "/") return new Response("not found", { status: 404 });
+
+		if (request.method !== "POST") return html(null);
+
+		const form = await request.formData();
+		const raw = form.get("page");
+		const page = typeof raw === "string" ? Number.parseInt(raw, 10) : Number.NaN;
+		if (Number.isNaN(page) || page < 1 || page > 10000) {
+			return html(null, "enter a page number (1 = most recently updated)");
+		}
+		const result = await dumpPage(env, page);
+		return html(result.status, result.detail, String(page));
+	},
+};
+
+async function dumpPage(env: Env, page: number): Promise<{ status: string; detail: string }> {
+	const cookie = env.DIIGO_COOKIE;
+	if (!cookie) {
+		return { status: "no-creds", detail: "Missing DIIGO_COOKIE.\nSet it: npx wrangler secret put DIIGO_COOKIE (or .dev.vars locally)." };
+	}
+
+	const target = new URL(DIIGO_BASE + "load_user_items");
+	target.searchParams.set("sort", "updated");
+	target.searchParams.set("count", String(COUNT));
+	target.searchParams.set("page_num", String(page));
+
+	try {
+		const response = await fetch(target.toString(), {
+			headers: {
+				cookie,
+				// same attenuated-request shape diigo-dump.mjs / diigo-interact.mjs use
+				"user-agent": "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/141.0",
+				"x-requested-with": "XMLHttpRequest",
+			},
+		});
+		const text = await response.text();
+		let detail = text;
+		if (response.ok) {
+			try {
+				const data = JSON.parse(text) as { total?: number; items?: unknown[] };
+				detail = `total: ${data.total}\nitems: ${data.items?.length ?? 0}\n\n${JSON.stringify(data.items ?? data, null, 2)}`;
+			} catch {
+				// non-JSON; show raw
+			}
+		}
+		return { status: String(response.status), detail };
+	} catch (err) {
+		return { status: "error", detail: String(err) };
+	}
+}
+
+function html(status: string | null, detail?: string, page?: string): Response {
+	const form = `
`; + const result = + status === null + ? "" + : `

status: ${status}

${escapeHtml(detail ?? "")}
`; + return new Response( + ` +diigo dump + +

diigo dump

+${form} +${result} +`, + { headers: { "content-type": "text/html;charset=utf-8" } }, + ); +} + +function escapeHtml(s: string): string { + return s + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} \ No newline at end of file diff --git a/diigo-dump/wrangler.jsonc b/diigo-dump/wrangler.jsonc new file mode 100644 index 0000000..0514f9f --- /dev/null +++ b/diigo-dump/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "diigo-dump", + "main": "src/index.ts", + "compatibility_date": "2026-09-29", + "observability": { "enabled": true } +} \ No newline at end of file