A list of titles and addresses is what a browser's own bookmark bar does. The reason to keep pages here is that they have been fetched and archived, and a grid of covers is the cheapest way to show that. ── Which picture, and the trade it costs ── A crawled bookmark carries two: `content.imageUrl`, the page's own og:image, and a `screenshot` asset Karakeep took and stores itself. The screenshot is better - archived, cannot rot, never leaves the house - and it is unreachable from an `<img>`: Officer authenticates with a bearer token in a header and a browser cannot put a header on an image request, so the asset route answers 401 from markup. Fetching each as a blob would work and costs a request, a decode and an object URL per card. So the origin's image, with referrerPolicy="no-referrer" so those sites learn nothing about where the request came from, and loading="lazy" so a long grid does not fetch a hundred at once. The honest cost, written at the point of use: viewing your bookmarks makes requests to the sites they point at. Proxying the archived asset is the fix if that ever matters. ── The empty state is three states ── Not crawled yet, crawled with nothing to find, and a picture that failed to load. The first is temporary and the others are not, so it says "fetching…" and the header counts how many are still going. Measured on the live instance while writing this: 6 of 50 crawled, the rest pending - so a fresh import spends a while looking like a broken panel unless it explains itself. Grid is auto-fill on a minimum width rather than a column count, because this is one pane of a layout the owner can drag and the columns have to follow the width it is actually given.
75 lines
3.5 KiB
TypeScript
75 lines
3.5 KiB
TypeScript
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://<officer>/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.02',
|
|
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/*'],
|
|
};
|