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)
This commit is contained in:
commit
126aab5cbe
4 files changed
+168
No files matched your search
+11
@@ -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
|
||||||
@@ -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 `<pre>`.
|
||||||
|
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="<paste Cookie header from diigo.com devtools>"
|
||||||
|
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.
|
||||||
@@ -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<Response> {
|
||||||
|
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 = `<form method="post" action="/"><label>page <input type="number" name="page" min="1" value="${page ?? 1}"></label> <button type="submit">dump</button></form>`;
|
||||||
|
const result =
|
||||||
|
status === null
|
||||||
|
? ""
|
||||||
|
: `<p>status: <b>${status}</b></p><pre readonly style="white-space:pre-wrap;border:1px solid #ccc;padding:1rem;max-height:60vh;overflow:auto">${escapeHtml(detail ?? "")}</pre>`;
|
||||||
|
return new Response(
|
||||||
|
`<!doctype html>
|
||||||
|
<html><head><meta charset="utf-8"><title>diigo dump</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>diigo dump</h1>
|
||||||
|
${form}
|
||||||
|
${result}
|
||||||
|
</body></html>`,
|
||||||
|
{ headers: { "content-type": "text/html;charset=utf-8" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s: string): string {
|
||||||
|
return s
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """);
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user