/** * @file diigo-dump worker: cookie access to diigo's * interact_api/load_user_items via a WebApp Durable Object instance from * the webapp worker. The id is a bearer capability held in DIIGO_WEB_ID; * the cookie lives only inside that DO's storage. * * Connect flow (no askpass, no cookie-on-disk): the home page links to * /_connect, which proxies webapp's mint form with the diigo prefills * (cookie field blank) — paste the cookie, mint lands via the service * binding, and the page shows the DIIGO_WEB_ID to put in .dev.vars. * (scripts/diigo-login automates the same flow for terminals.) * @see {@link DiigoBridge} */ import { WorkerEntrypoint } from "cloudflare:workers"; import { dumpBookmarks, type PagedGet } from "./diigo.js"; /** the WEBAPP service binding to webapp/src/index.ts:WebAppFactory; typed * structurally here pending a shared types package */ type WebAppFactoryShape = { newWebApp(init: { url: string; headers?: Record; msPerCall?: number; }): Promise<{ id: string; app: PagedGet }>; at(id: string): Promise; }; type Env = { DIIGO_WEB_ID?: string; WEBAPP: WebAppFactoryShape; }; const UA = "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/141.0"; const DIIGO_INTERACT = "https://www.diigo.com/interact_api/"; /** prefill for webapp's mint form: everything but the cookie */ const DIIGO_PREFILL = JSON.stringify({ "user-agent": UA, "x-requested-with": "XMLHttpRequest", }); /** connect to the configured instance, or give the setup pointer */ async function instance(env: Env): Promise { const id = env.DIIGO_WEB_ID; if (!id) { throw new Error("no instance: use the connect flow on the home page, then set DIIGO_WEB_ID"); } return env.WEBAPP.at(id); } export class DiigoBridge extends WorkerEntrypoint { /** the diigo-attenuated handle: fluent chain over the instance */ open(): Promise { return instance(this.env as unknown as Env); } /** bulk dump from the caller's address space */ async dumpNdjson(pages: number, count = 24): Promise { const diigoAPI = await instance(this.env as unknown as Env); const lines: string[] = []; for await (const item of dumpBookmarks({ diigoAPI, pages, count })) { lines.push(JSON.stringify(item)); } return lines.join("\n"); } } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); // the connect flow: proxy webapp's mint form (prefilled, cookie blank) if (url.pathname === "/_connect") { return env.DIIGO_WEB_ID ? connectedPage(env) : connectPage(); } // webapp's form POSTs back through us: forward to the binding if (url.pathname === "/_mint" && request.method === "POST") { return proxyMint(request, env); } if (url.pathname !== "/") return new Response("not found", { status: 404 }); if (request.method !== "POST") return home(null, undefined, Boolean(env.DIIGO_WEB_ID)); 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 home("enter a page number (1 = most recently updated)", undefined, Boolean(env.DIIGO_WEB_ID)); } try { const app = await instance(env); const text = await app .pathjoin("load_user_items") .query({ sort: "updated", count: 24, page_num: page }) .get(); const data = JSON.parse(text) as { total?: number; items?: Record[] }; return home( `total: ${data.total}\nitems: ${data.items?.length ?? 0}\n\n${JSON.stringify(data.items ?? data, null, 2)}`, undefined, true, ); } catch (err) { console.error({ event: "dump", page, error: String(err) }); return home(null, String(err), Boolean(env.DIIGO_WEB_ID)); } }, }; /** the connect page: webapp's form with diigo defaults. The mint POST is * proxied (/_mint) so one port serves the whole flow; the response * page shows the id and the .dev.vars line to add. */ async function connectPage(): Promise { // proxy: render our own form with action="/_mint" so one port serves // the whole flow. TODO(webapp): one canonical mint form; consider // serving it from webapp directly. const body = ` connect diigo session

connect diigo session

  1. copy the Cookie header from diigo.com devtools
  2. paste below, mint, then add the DIIGO_WEB_ID line it shows to .dev.vars (deployed: wrangler secret put DIIGO_WEB_ID)
  3. restart wrangler, dump from the home page

`; return new Response(body, { headers: { "content-type": "text/html;charset=utf-8" } }); } async function connectedPage(env: Env): Promise { let health = ""; try { const app = await instance(env); // a cheap call: one item, exercises the cookie + rate gate const text = await app .pathjoin("load_user_items") .query({ sort: "updated", count: 1, page_num: 1 }) .get(); const data = JSON.parse(text) as { total?: number }; health = `

instance is live: diigo reports ${esc(String(data.total ?? "?"))} bookmarks.

`; } catch (err) { console.error({ event: "connect-check", error: String(err) }); health = `

instance is configured but the last check failed:

${esc(String(err))}

If diigo expired the cookie, re-mint below.

`; } const body = ` diigo session

diigo session

connected: id ${esc(`${env.DIIGO_WEB_ID!.slice(0, 6)}…${env.DIIGO_WEB_ID!.slice(-4)}`)} (from .dev.vars).

${health}

replace the session

Only needed if the cookie is stale (diigo will say 401/403). Minting a new instance invalidates nothing, but the id rotates and .dev.vars needs the new DIIGO_WEB_ID.

`; return new Response(body, { headers: { "content-type": "text/html;charset=utf-8" } }); } /** proxy the mint: merge the pasted cookie into the headers JSON, mint via * the service binding, show the id (plus the .dev.vars line to copy) */ async function proxyMint(request: Request, env: Env): Promise { const form = await request.formData(); const cookie = String(form.get("cookie") || ""); if (!cookie) { return new Response("no cookie supplied", { status: 400 }); } const headers = { "user-agent": UA, "x-requested-with": "XMLHttpRequest", cookie, }; const { id } = await env.WEBAPP.newWebApp({ url: DIIGO_INTERACT, headers, msPerCall: 500, }); return idPage(id); } /** the id shown on the success page: a narrow text field (scrollable but * too small to show most of the value) with a copy button for the full * version. navigator.clipboard needs the page served from localhost/https * — true in dev and at the deployed URL */ function idPage(id: string): Response { const body = ` minted

minted

id:

add to .dev.vars:

DIIGO_WEB_ID=

…click copy, then paste the full value after it. back to diigo dump (restart wrangler first).

`; return new Response(body, { headers: { "content-type": "text/html;charset=utf-8", // for scripts: diigo-login reads this instead of scraping HTML "x-diigo-web-id": id, }, }); } function home(detail: string | null, error?: string, connected?: boolean): Response { const form = `
`; const result = error ? `

status: error

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

diigo dump

${connected ? "diigo session status/replace…" : "connect diigo session…"}

${form} ${result} `, { headers: { "content-type": "text/html;charset=utf-8" } }, ); } function esc(s: string): string { return s .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } const escapeHtml = esc;