# One extension directory Extensions are currently presented as two kinds. They should be one kind with different provenance. This is what that costs and where the seams actually are. The goal, stated plainly: **one catalogue, one lifecycle.** Every extension can be installed, removed, turned on and turned off by the same actions. Some arrive already installed. Who wrote it and who signed it is a fact *about* an extension — searchable, shown on its card — not a wall between two lists. ## The good news: the runtime is already unified None of the split is in the engine. - Every extension lives in one directory, `/extensions/`. A pre-installed one and a downloaded one land in the same place, side by side. `seedBundledAddons()` even says so: *"the framework treats bundled and user add-ons identically, no special path"*. - One discovery pass activates them all — `AddonHost.discoverAndActivate()` scans that single directory. - Enable and disable are already uniform: `settings.disabledAddons` is a list of ids, and `addons-set-enabled` does not care where an extension came from. So this is not a rewrite. It is removing four specific things that impose a division the engine never had. ## Seam 1 — removal does not stick, so the UI hides it `seedBundledAddons()` copies any bundled folder whose target is missing. Delete a pre-installed extension and it is back on the next launch. That single fact is why the UI marks them unremovable, and the comment on `addons-list` admits the motive: *"Mark bundled add-ons so the extensions UI can render them differently… look built-in rather than removable extensions."* Worse, there is no uninstall at all — for anything. The only removal route is `addons-reveal`, opening the folder in the file manager, described in the source as *"the primary way users edit / uninstall add-ons"*. **Fix.** A tombstone list, `settings.removedExtensions: string[]`. - Seeding skips any id on it. - Uninstall adds the id; installing again clears it. Then removal means something for every extension, and a user who removes a pre-installed one does not have it resurrected by the next update. **Fix.** An `extensions-remove` handler that deactivates the extension, moves its folder into the existing `extensions-backups/--/` rather than deleting outright, tombstones the id and re-runs discovery. Its stored data in `extensions-data/.json` survives by default so a reinstall keeps settings, with an explicit "also delete its data" option for people who mean it. ## Seam 2 — two catalogues, and one of them cannot install There are two publishing shapes today: | | Where | Signed by | Indexed? | Installable? | |---|---|---|---|---| | Operator channel | `bns/theseus/extensions//` | operator Ed25519 key | no index, per-id only | **no** | | Community | `bns/theseus/extensions/community//` | publisher's BCH key, checked against the name's on-chain owner | `/api/ext/catalog` | yes | Every install route — the Settings button and the `theseus://extensions/install/` deep link — goes through `installCommunityById`, which resolves the id against the community catalogue. `main.js` has no operator-channel install path at all. Two consequences, both live right now: - The **Install in Theseus** buttons added to the Screenshot and PDF Editor cards do not work. Those ids are not in the community catalogue. It goes unnoticed only because both already ship in the build. - Moving an extension from the community channel to the operator channel makes it **uninstallable** unless it also ships in the build. This is exactly the trap the Word editor migration is walking into. **Fix.** One catalogue endpoint listing everything, with the signing scheme as a field rather than as a separate world. `/api/ext/catalog` keeps its URL and merges both prefixes, each entry carrying: ```jsonc { "id": "pdf-editor", "publisher": "Silent Mode", // display name of the developer "signer": { "kind": "operator" }, // or { kind: "name", name: "theseus.x", owner: "bchtest:…" } "channel": "https://…/extensions/pdf-editor/updates.json", "latest": "0.2.0", "sha256": "…" } ``` and one `installExtension(id)` in main.js that resolves from it and verifies with whichever key the entry declares. That is the smallest change that makes installation uniform, and it repairs the dead buttons on the way past. ## Seam 3 — two lists in Settings Settings has an Extensions section and a separate Plug-ins section, divided by `manifest.category === "plugin"` so that Aegis and friends *"look built-in"*. That is the same division again, by a different field. **Fix.** One list. Category becomes a filter value, not a page. Alongside it: - free-text search over name, description and developer - filter chips: All · Enabled · Disabled · Pre-installed · by developer - each card shows developer, version, where it updates from, and the same four actions for every row ## Seam 4 — the website mirrors the split `site-theseus-x/extensions/index.html` hardcodes a card per shipped extension and renders community ones from `catalog.json` into a separate section below. Adding an extension means hand-writing HTML. **Fix.** Render every card from the one catalogue. The page already has the machinery — it fills version, SHA-256 and tarball URL live from each `updates.json` — it just needs one loop over one list instead of two. ## The record an extension carries `AddonHost.snapshot()` reports id, name, version, description, author, icon, capabilities, category, folder, enabled and error. `main.js` then bolts on `bundled` by looking for a same-named folder in `resources/bundled-addons/`. Replace that flag and add the provenance the new UI wants to search on: - `publisher` — who signed the channel - `origin` — `"preinstalled"` or `"installed"`, a badge and nothing more - `channel` — its `updates.json`, so a card can say where updates come from - `installedAt`, `updatedAt` - `removable` — true for everything, once tombstones exist `bundled` goes away. Nothing downstream should branch on where an extension came from. ## One thing not to flatten "Who the developer is" should not split the catalogue, and it should not be hidden either. An extension signed by the Silent Mode operator key and one signed by a stranger's wallet carry the same permissions once installed, and they are not equally safe. The consent dialog already gets this right — it names the publisher and says the extension *"runs with the same access as any add-on"*. One list, one lifecycle, and the signer stated plainly on the card and in the prompt. ## Order of work Each step stands on its own and leaves the product working. 1. **Tombstones and `extensions-remove`.** Removal becomes real for every extension. No UI change yet. 2. **One catalogue and one `installExtension`.** Fixes the dead Install buttons and unblocks the Word editor migration. 3. **One Settings list** with search and filters; Plug-ins becomes a filter. 4. **Site page renders from the catalogue** instead of hardcoded cards. 5. **Drop `bundled`** from `addons-list` and the last branches on it. Step 2 is the one with a deadline attached: until it lands, nothing should be removed from the community catalogue, because that is the only route by which an extension can be installed.