import type { PluginManifest } from '@@/plugins/manifest'; /** * Karakeep — your bookmarks, behind an Officer session. * * ── The model, which is Vault's ── * * One Karakeep per Officer install, pointed at by the owner. Officer stores no bookmarks: Karakeep owns * those. What this plugin owns is the CREDENTIAL and the door. Where Vault brokers one account, this maps * every Officer user onto a Karakeep account of their own — `db/schema.ts`, and * `plugins/USER_PROVISIONING.md` for the contract that governs creating one. * * ── Why Karakeep and not Linkwarden, which this was first built against ── * * Worth recording, because the plugin is otherwise the same shape and somebody will wonder. * * Linkwarden authenticates its extension through NextAuth: host-scoped session cookies, CSRF checked * against the host, and absolute URLs baked from `NEXTAUTH_URL`. Proxying that means the upstream has to * be told the address the CLIENT uses, which is not the address the sidecar dials, and getting it wrong * bounces the extension out of the proxy mid-login. It cost a NEXTAUTH_URL change, a lost csrf cookie and * a container recreate, and it retired the upstream's own web login as the price of going through Officer. * * Karakeep's extension holds an API KEY and sends it as a header. Nothing is host-pinned, so none of the * above exists: the same key works at any address, and the upstream stays reachable on its own. * * ── The extension, and why one prefix is enough ── * * It calls `${address}/api/trpc`, so pointing it at `https:///api/karakeep/kk` puts everything * it needs under one declared path. No root-path claims, and no header routing. * * A bare Officer origin — the Bitwarden arrangement, where you paste the Officer URL and nothing else — * is possible here and is NOT declared yet. It would need `protocolHeaders`, and Karakeep's extension can * carry one because its settings take arbitrary `customHeaders`. That is a real option rather than a * wish, and it is left until the path form is proven, because it asks the owner to configure a header * whose purpose is invisible from where they are typing it. * * ── No `osDependencies` ── * * Nothing shells out. The sidecar is a Bun HTTP proxy and the upstream is reached with `fetch`. */ export const manifest: PluginManifest = { publisher: 'officerdev', version: '2026.09.05.04', platform: '>=1.0.0', label: 'Karakeep', summary: 'Your bookmarks, behind an Officer session — saved, archived and searchable', tags: ['Personal', 'Degoogling'], icon: 'Bookmark', color: '#14b8a6', permissions: [ { key: 'karakeep', label: 'Karakeep', description: 'Your saved bookmarks, lists and their archived pages', /* Reading and saving is every user's. Pointing the install at a server and creating accounts on it is not — `/config` holds the address the whole install uses and `/accounts` provisions against an admin credential. Both are prefix-matched, so the trees beneath them are covered. */ ownerOnly: ['/config', '/accounts'], }, ], /* The extension's door, served without an Officer session. A stock Karakeep extension carries its own API key and has nowhere to put an Officer JWT. Same argument as Vault's `/bw`: it is the same exposure through one address instead of two, and it is what lets Karakeep stop being reachable on its own. */ unauthenticatedPaths: ['/kk/*'], };