Files
pastilhas 2b3b986c0e settings belong at the bottom of the sidebar, not behind a gear in the header
The owner asked for this after failing to find it, which is the whole argument: a
gear in a header is where a settings control is easy to write and not where
anybody looks for one. The bottom of a sidebar is its conventional home, and a
labelled row rather than an icon means it says what it is.

The form opens ABOVE the button so the control does not move when you use it. A
disclosure that walks away from the pointer as it expands is the small kind of
wrong that makes a thing feel unfinished.

`aria-expanded` on the button, because it is a disclosure and a screen reader has
no other way to know the state of one.

The error path still force-opens it - "no account is mapped to you" is the state
every user starts in, and the fix is in that form.
2026-09-05 21:49:09 +01:00

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.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/*'],
};