theseus/docs/DESIGN-one-extension-catalogue.md
Local Dev 18eee9dbc7 feat(docx-editor): blue page with a pilcrow, matching pdf-editor's badge
The mark now shares pdf-editor's silhouette, corner geometry and weight, in
blue against its red, so the two editors read as a pair in the dock. It
carries a pilcrow rather than a format label: at 16px "DOC" is a smudge
while the paragraph mark is still a glyph, and it says word processor rather
than file extension.

Drawn as paths, not <text>. An icon that needs a particular font installed
to make sense is an icon that eventually renders wrong somewhere.

Still nothing of Microsoft's: their blue sheet carries a white W, and the
page-with-a-folded-corner is the universal document glyph rather than
anyone's property.

Also adds docs/DESIGN-one-extension-catalogue.md, which writes down the
larger point this kept bumping into — that bundled and community extensions
are two systems for one kind of thing, with two trust rules and two lists in
Settings, and that "ships by default" should be a line of configuration
rather than a separate distribution path. Proposed only; the awkward part is
keeping a fresh offline install usable, which the note answers by treating
the shipped copies as a pre-seeded cache that still verifies like everything
else.
2026-09-22 00:06:50 +02:00

4.6 KiB

One catalogue, one set of rules

Status: proposed, 2026-09-22. Not implemented.

Theseus currently runs two extension systems that happen to produce the same kind of thing.

Bundled add-ons Community extensions
How it arrives compiled into the build, seeded into the profile on first run installed from the gateway catalogue
Who signs the Silent Mode operator's Ed25519 key, pubkeys compiled into the app the owner of a BNS name, checked against the chain
Update channel theseus.x/extensions/<id>/updates.json theseus.x/extensions/community/<id>/updates.json
In Settings a section headed "Built into Theseus" a separate section headed "Community"
How to publish scripts/sign-addon-update.mjs + sia-upload the publish page, or PUT /api/ext/…

Two catalogues in the UI, two trust rules, two publishing paths — for a difference that is really only "who wrote it" and "is it there on day one".

What this should be instead

One catalogue. Every extension is listed in the same place, whoever wrote it. Silent Mode publishes under a Silent Mode name the same way anyone else publishes under theirs. Being first-party is a fact about the publisher, not a separate distribution system, and the UI should say so with a line of text rather than a second list.

One rule for trust. An extension is installable because a signature checks out against the current owner of the publisher's name. That is already what verifyPublisherEntry does; the operator Ed25519 key becomes either one more accepted signer or nothing at all. pickChannelEntry already accepts both, so this is mostly deletion.

Defaults are configuration. Which extensions a fresh Theseus starts with is a list of ids in the app's config, not a folder that gets copied. Changing the default set should be editing that list, not moving directories around and rebuilding.

The one thing that needs care

If defaults are fetched from the catalogue on first run, a fresh install with no network — or with the gateway down — has no editor, no wallet and no screenshot tool. That is a real regression from today, where the build carries them.

So keep shipping the code, but stop treating it as a different kind of thing:

  • The build continues to include the default extensions' folders. They are a pre-seeded cache, not a separate class of add-on.
  • On first run they are installed from that cache, then verified and updated against the catalogue exactly like anything the user installed by hand.
  • If a seeded copy fails verification, it is quarantined rather than run — a cache is only a cache.

That keeps offline first-run working while leaving one set of rules everywhere else.

Sketch of the work

  • Config: defaultExtensions: [{ id, publisher }] in the app's settings, read at first run and on profile migration. Ids the user has removed are remembered and not re-seeded — uninstalling a default must stick.
  • main.js: seeding installs through the normal install path instead of copying a folder and calling it built-in. One code path, one place where a signature is checked.
  • settings.html: one list. "Installed by default" becomes a badge on a row, next to the publisher's name. The separate Community section and its Install button fold into the single catalogue view.
  • addon-updater.js: drop the distinction between the two channel shapes; an extension has a publisher and an updates URL.
  • Gateway: the catalogue gains first-party entries. Nothing changes structurally — /api/ext already stores whatever a name owner signs.
  • Publishing: scripts/sign-addon-update.mjs and the operator key stop being the first-party route. Silent Mode's own extensions go through PUT /api/ext/<our-name>/<id>/<version> like everyone else's, which means the release process needs that name's wallet rather than a key file.

Open questions

  • Which name publishes Silent Mode's extensions? Whoever goes first owns each id permanently. Worth deciding once, deliberately.
  • Pinning. A default extension is installed without anyone choosing it, so a compromised or coerced catalogue entry reaches every fresh profile. Pinning the default set to a version and a publisher in the app's config bounds that.
  • Does the operator key survive at all? Keeping it as an accepted signer for the default set is a hedge against the name-owner key being lost; keeping it forever means the two-rules problem never fully goes away.