Compare commits
5 commits
7724dde2ef
...
ed98d0280c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed98d0280c | ||
|
|
fbde2f7650 | ||
|
|
3d9b05ba25 | ||
|
|
ed2d2e1d5a | ||
|
|
0da15818ad |
|
|
@ -687,9 +687,15 @@ class AddonHost {
|
|||
toolbarMenus: this.getToolbarMenus(),
|
||||
};
|
||||
}
|
||||
// `plugin` marks surfaces of a first-class Silent Mode component (manifest
|
||||
// category "plugin", e.g. Aegis): the chrome pins those to their own dock
|
||||
// instead of the extensions row.
|
||||
getSidebarPanels() {
|
||||
const out = [];
|
||||
for (const active of this._active.values()) out.push(...active.sidebarPanels);
|
||||
for (const active of this._active.values()) {
|
||||
const plugin = active.manifest.category === "plugin";
|
||||
for (const p of active.sidebarPanels) out.push({ ...p, plugin, addonName: active.manifest.name });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Menu declarations from every active add-on that carries a toolbar-menu
|
||||
|
|
@ -704,6 +710,7 @@ class AddonHost {
|
|||
addonId: active.manifest.id,
|
||||
title: tm.title,
|
||||
icon: tm.icon,
|
||||
plugin: active.manifest.category === "plugin",
|
||||
items: tm.items.map((it) => ({ id: it.id, label: it.label, icon: it.icon })),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,11 +35,14 @@ vendored editor libraries.
|
|||
`icon` field, which is what the dock button and the catalogue card read — so
|
||||
there is never a second version to keep in step.
|
||||
|
||||
It is deliberately not Microsoft's Word icon. That mark, and the blue sheet
|
||||
with a white W it belongs to, are Microsoft's trademarks; dressing our
|
||||
extension up as theirs would be both a legal problem and a dishonest one. The
|
||||
mark here says "text document" in its own words: a page with a turned corner,
|
||||
a heading rule, body lines, and a pilcrow badge in Silent Mode green.
|
||||
It is a blue document badge, paired with pdf-editor's red one so the two
|
||||
editors read as a set in the dock — a solid fill stays legible at the 16–18px
|
||||
the dock actually renders, which an outlined page does not.
|
||||
|
||||
Deliberately not Microsoft's Word icon: their mark is a blue sheet carrying a
|
||||
white W, and dressing our extension up as theirs would be both a legal
|
||||
problem and a dishonest one. A folded-corner page is the universal document
|
||||
glyph, and "DOC" is the file format rather than anyone's trademark.
|
||||
|
||||
## Building
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"version": "0.1.1",
|
||||
"description": "Open, edit and save Word documents (.docx) in a full Theseus tab, or export them as PDF. Ribbon-style formatting, tables, lists, images and links; headers, footers, footnotes, page setup and the document's own styles are carried through a save untouched.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M6%202h8l6%206v12a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V4a2%202%200%200%201%202-2z%22%20fill%3D%22%232563eb%22%2F%3E%3Cpath%20d%3D%22M14%202v6h6z%22%20fill%3D%22%231e40af%22%2F%3E%3Cg%20fill%3D%22%23fff%22%3E%3Cpath%20d%3D%22M12.3%208.6a3.1%203.1%200%200%200%200%206.2z%22%2F%3E%3Crect%20x%3D%2212.05%22%20y%3D%228.6%22%20width%3D%221.7%22%20height%3D%2210.8%22%20rx%3D%22.2%22%2F%3E%3Crect%20x%3D%2214.85%22%20y%3D%228.6%22%20width%3D%221.7%22%20height%3D%2210.8%22%20rx%3D%22.2%22%2F%3E%3Crect%20x%3D%2212.05%22%20y%3D%228.6%22%20width%3D%224.5%22%20height%3D%221.7%22%20rx%3D%22.2%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E",
|
||||
"icon": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M6%202h8l6%206v12a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V4a2%202%200%200%201%202-2z%22%20fill%3D%22%232563eb%22%2F%3E%3Cpath%20d%3D%22M14%202v6h6z%22%20fill%3D%22%231e40af%22%2F%3E%3Ctext%20x%3D%2212%22%20y%3D%2217%22%20text-anchor%3D%22middle%22%20font-family%3D%22system-ui%2C-apple-system%2Csans-serif%22%20font-size%3D%225.5%22%20font-weight%3D%22800%22%20fill%3D%22%23fff%22%3EDOC%3C%2Ftext%3E%3C%2Fsvg%3E",
|
||||
"main": "index.js",
|
||||
"capabilities": [
|
||||
"sidebar-panel",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 576 B After Width: | Height: | Size: 634 B |
|
Before Width: | Height: | Size: 680 B After Width: | Height: | Size: 711 B |
|
|
@ -1,27 +1,18 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<!--
|
||||
Silent Mode's mark for the Word editor.
|
||||
Silent Mode's mark for the Word editor: a blue document badge, paired
|
||||
with pdf-editor's red one so the two editors read as a set in the dock.
|
||||
Originally drawn in commit 95c93f9 alongside the PDF badge; kept here as
|
||||
the single source that addon-build/docx-editor/make-icons.mjs rasterises
|
||||
to the PNGs and writes into addon.json.
|
||||
|
||||
Deliberately not Microsoft's Word icon. That mark, and the blue sheet
|
||||
with a white "W" it belongs to, are Microsoft's trademarks; dressing our
|
||||
extension up as theirs would be both a legal problem and a dishonest one.
|
||||
|
||||
It shares pdf-editor's page silhouette, corner geometry and weight so the
|
||||
two editors read as a set in the dock, and carries a pilcrow — the
|
||||
paragraph mark every word processor has used since the trade was done on
|
||||
paper — rather than a format label. The glyph is drawn as paths, not
|
||||
<text>: an icon that needs a particular font installed to make sense is
|
||||
an icon that eventually renders wrong somewhere.
|
||||
Deliberately not Microsoft's Word icon. Their mark is a blue sheet
|
||||
carrying a white "W", and dressing our extension up as theirs would be
|
||||
both a legal problem and a dishonest one. A folded-corner page is the
|
||||
universal document glyph, and "DOC" is the file format, not anyone's
|
||||
trademark.
|
||||
-->
|
||||
<path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" fill="#2563eb"/>
|
||||
<path d="M14 2v6h6z" fill="#1e40af"/>
|
||||
<g fill="#fff">
|
||||
<!-- bowl: a half disc hanging off the left stem -->
|
||||
<path d="M12.3 8.6a3.1 3.1 0 0 0 0 6.2z"/>
|
||||
<!-- the two stems -->
|
||||
<rect x="12.05" y="8.6" width="1.7" height="10.8" rx=".2"/>
|
||||
<rect x="14.85" y="8.6" width="1.7" height="10.8" rx=".2"/>
|
||||
<!-- top bar tying the bowl and stems together -->
|
||||
<rect x="12.05" y="8.6" width="4.5" height="1.7" rx=".2"/>
|
||||
</g>
|
||||
<text x="12" y="17" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="5.5" font-weight="800" fill="#fff">DOC</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1,017 B |
66
chrome.html
|
|
@ -335,7 +335,10 @@
|
|||
.engine-btn .esym { font-size: 14px; line-height: 1; }
|
||||
.engine-btn .cv { font-size: 8px; color: var(--dim); }
|
||||
#search { flex: 1; padding: 7px 4px; }
|
||||
.tor { padding: 7px 10px; border-radius: 8px; border: 1px solid var(--line); background: #2b2f3a; color: var(--ink); cursor: pointer; white-space: nowrap; font-size: 12.5px; }
|
||||
/* Tor toggle lives on the favorites row beside the plug-in dock (same
|
||||
chip height as .plugbtn); the tint still says off / connecting / on. */
|
||||
.tor { display: inline-flex; align-items: center; height: 22px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--line);
|
||||
background: #2b2f3a; color: var(--ink); cursor: pointer; white-space: nowrap; font: inherit; font-size: 12px; font-weight: 600; }
|
||||
.tor.connecting { background: #7a5c14; } .tor.on { background: #6b3fa0; }
|
||||
.logo { display: flex; align-items: center; gap: 6px; font-weight: 700; color: var(--acid); white-space: nowrap;
|
||||
padding: 6px 12px; border: 1px solid color-mix(in srgb, var(--acid) 20%, transparent); border-radius: 8px; cursor: pointer; background: rgb(from var(--acid) r g b / .08); font-size: 13px; }
|
||||
|
|
@ -345,6 +348,18 @@
|
|||
uniform fixed width so the row reads as a neat grid regardless of the
|
||||
underlying title length; the strip is 28px tall to fit the roomier chips. */
|
||||
.bookmarks { display: flex; align-items: center; gap: 6px; padding: 4px 10px 6px; height: 28px; overflow: hidden; }
|
||||
#bmlist { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; overflow: hidden; }
|
||||
/* Plug-in dock: first-class Silent Mode components (Aegis) get a labelled
|
||||
chip at the right end of the favorites row, directly under the Theseus
|
||||
menu — always visible, never collapsed with the extensions. */
|
||||
.bmright { margin-left: auto; flex: none; display: flex; align-items: center; gap: 6px; padding-left: 8px; }
|
||||
.plugdock { display: flex; align-items: center; gap: 4px; }
|
||||
.plugbtn { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 10px 0 8px; border-radius: 6px;
|
||||
font: inherit; font-size: 12px; font-weight: 600; color: var(--mut); cursor: pointer; white-space: nowrap;
|
||||
border: 1px solid var(--line); background: transparent; }
|
||||
.plugbtn:hover { background: var(--line2); color: var(--ink); }
|
||||
.plugbtn.active { background: rgb(from var(--acid) r g b / .12); color: var(--acid); border-color: color-mix(in srgb, var(--acid) 35%, transparent); }
|
||||
.plugbtn img { display: block; }
|
||||
/* Ctrl+F find-in-page bar. Sits below the address bar (above tabs body),
|
||||
~32px tall so it's part of the chrome view height like .tordisc / .bcnrbar.
|
||||
syncHeight in the tabs handler picks it up automatically. */
|
||||
|
|
@ -473,11 +488,10 @@
|
|||
<button class="extmore" id="extmore" title="Extensions" aria-label="Extensions"></button>
|
||||
</span>
|
||||
<span class="upchip" id="upchip" hidden><button class="upcore" id="upDownload" title=""></button><button class="updismiss" id="upDismiss" title="Not now">✕</button></span>
|
||||
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
|
||||
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear">⚙</span></button>
|
||||
</div>
|
||||
<div class="loadbar" id="loadbar"></div>
|
||||
<div class="bookmarks" id="bookmarks" hidden></div>
|
||||
<div class="bookmarks" id="bookmarks" hidden><div id="bmlist"></div><span class="bmright"><span class="plugdock" id="plugdock" hidden></span><button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button></span></div>
|
||||
<div class="findbar" id="findbar" hidden>
|
||||
<input id="findInput" type="text" placeholder="Find in page" spellcheck="false" autocomplete="off">
|
||||
<span class="findcount" id="findCount">–</span>
|
||||
|
|
@ -666,15 +680,21 @@
|
|||
// an addon that declares a toolbar-menu both get one entry here. panels
|
||||
// stay clickable-toggle; menus open a floating dropdown positioned under
|
||||
// the icon and dispatch the picked item back to the add-on.
|
||||
let lastPanels = [], lastMenus = [], lastButtons = [];
|
||||
let lastPanels = [], lastMenus = [], lastButtons = [], lastAllPanels = [], lastAllMenus = [];
|
||||
let sidebarOpen = false, activePanelId = null;
|
||||
let openMenuAddonId = null; // which addon's menu dropdown is currently open
|
||||
function renderSidebarState(d) {
|
||||
if (!d) return;
|
||||
lastPanels = Array.isArray(d.panels) ? d.panels : [];
|
||||
lastMenus = Array.isArray(d.toolbarMenus) ? d.toolbarMenus : [];
|
||||
const allPanels = Array.isArray(d.panels) ? d.panels : [];
|
||||
const allMenus = Array.isArray(d.toolbarMenus) ? d.toolbarMenus : [];
|
||||
sidebarOpen = !!d.visible;
|
||||
activePanelId = d.active || null;
|
||||
// Plug-ins (Aegis) live in their own dock on the favorites row, under
|
||||
// the Theseus menu; everything else is an extension in the toolbar dock.
|
||||
lastAllPanels = allPanels; lastAllMenus = allMenus;
|
||||
lastPanels = allPanels.filter((p) => !p.plugin);
|
||||
lastMenus = allMenus.filter((m) => !m.plugin);
|
||||
renderPlugDock(allPanels.filter((p) => p.plugin), allMenus.filter((m) => m.plugin));
|
||||
// Build the flat button list, panels first (matches shipped order) then
|
||||
// toolbar-menu buttons in registration order.
|
||||
lastButtons = [];
|
||||
|
|
@ -724,6 +744,36 @@
|
|||
$("extmore").classList.toggle("active", (sidebarOpen && !!activePanelId) || !!openMenuAddonId);
|
||||
}
|
||||
|
||||
// Plug-in dock (favorites row, right end). A chip per plug-in surface:
|
||||
// icon + name, tinted while its sidebar panel or menu is open. Same
|
||||
// actions as the toolbar dock buttons, never collapsed.
|
||||
function renderPlugDock(panels, menus) {
|
||||
const dock = $("plugdock");
|
||||
const chips = [];
|
||||
// Chip label is the plug-in's product name (e.g. "Aegis Wallet"), the
|
||||
// panel title goes in the tooltip.
|
||||
for (const p of panels) chips.push({ kind: "panel", panelId: p.panelId, icon: p.icon, title: p.title || "Plug-in", name: p.addonName || p.title || "Plug-in", active: sidebarOpen && p.panelId === activePanelId });
|
||||
for (const m of menus) chips.push({ kind: "menu", addonId: m.addonId, icon: m.icon, title: m.title || m.addonId, name: m.title || m.addonId, active: openMenuAddonId === m.addonId });
|
||||
dock.hidden = chips.length === 0;
|
||||
dock.innerHTML = chips.map((c) => {
|
||||
const dataAttr = c.kind === "panel" ? `data-panel="${c.panelId.replace(/"/g,""")}"` : `data-menu="${c.addonId.replace(/"/g,""")}"`;
|
||||
const label = String(c.name).split(/\s+[—–-]\s+/, 1)[0].replace(/</g,"<");
|
||||
return `<button class="plugbtn${c.active ? " active" : ""}" ${dataAttr} title="${c.title.replace(/"/g,""")}">${addonIconHtml(c.icon, 14)}<span>${label}</span></button>`;
|
||||
}).join("");
|
||||
dock.querySelectorAll(".plugbtn").forEach((btn) => {
|
||||
btn.onclick = () => {
|
||||
if (btn.dataset.panel) {
|
||||
const pid = btn.dataset.panel;
|
||||
closeToolbarMenu();
|
||||
if (sidebarOpen && pid === activePanelId) { T.closeSidebar && T.closeSidebar(); }
|
||||
else { T.openSidebar && T.openSidebar(pid); }
|
||||
} else if (btn.dataset.menu) {
|
||||
openToolbarMenu(btn.dataset.menu, btn);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---- toolbar-menu dropdown (per-addon) ----
|
||||
// Positions under the clicked icon, styled after .extpop. Clicking an
|
||||
// item dispatches to main via addonMenuSelect(addonId, itemId) and
|
||||
|
|
@ -734,7 +784,7 @@
|
|||
if (openMenuAddonId) {
|
||||
openMenuAddonId = null;
|
||||
// Re-render dock so the "active" tint drops off the icon.
|
||||
renderSidebarState({ visible: sidebarOpen, active: activePanelId, panels: lastPanels, toolbarMenus: lastMenus });
|
||||
renderSidebarState({ visible: sidebarOpen, active: activePanelId, panels: lastAllPanels, toolbarMenus: lastAllMenus });
|
||||
}
|
||||
}
|
||||
function openToolbarMenu(addonId, anchorBtn) {
|
||||
|
|
@ -933,7 +983,7 @@
|
|||
return bookmarkBrandCase(cut);
|
||||
}
|
||||
function renderBookmarks() {
|
||||
const box = $("bookmarks");
|
||||
const box = $("bmlist");
|
||||
box.innerHTML = bookmarks.length
|
||||
? bookmarks.map((b, i) => {
|
||||
const fav = b.favicon
|
||||
|
|
|
|||
|
|
@ -35,6 +35,49 @@ 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.
|
||||
|
||||
## Which keys may sign what
|
||||
|
||||
Multiple signing keys already work, and there is already an allow-list: the
|
||||
`PUBKEYS_HEX` array in `addon-update-pubkeys.js`. `verifySignature` walks it
|
||||
and accepts an update if any entry verifies, so adding a second operator key
|
||||
is a line in that file plus a Theseus release. The file's own comment sets
|
||||
out the rotation: sign with both, ship a release carrying both, then drop
|
||||
the old one.
|
||||
|
||||
The problem is not that the list is missing. It is that the list is
|
||||
**global**. Every add-on is verified against the same array, so any key on
|
||||
it can sign an update for any id — a key meant for the screenshot tool can
|
||||
push code for the wallet. The signed message is
|
||||
`silentmode.addon-update-v1|<id>|<version>|<sha256>`, which binds a
|
||||
signature to an extension, but nothing binds an extension to a signer.
|
||||
|
||||
The publisher path does not have this problem. An entry carrying
|
||||
`publisherSig` is checked against the current owner of that extension's
|
||||
name, so the permission is per-extension by construction and rotates on
|
||||
chain without an app release. That is the model to generalise, not replace.
|
||||
|
||||
So: keep an allow-list, and scope it.
|
||||
|
||||
- **Per-extension, not global.** Each extension names who may sign it — a
|
||||
BNS name for anything in the catalogue, or a pinned pubkey for a member of
|
||||
the default set. `verifySignature` takes that extension's keys instead of
|
||||
the global array, and its callers look the binding up by id.
|
||||
- **Plural per extension.** Two or three acceptable keys, any one
|
||||
sufficient, so a rotation or a lost key doesn't need an emergency release.
|
||||
- **Prefer the name-owner path for anything new.** The chain is an
|
||||
allow-list that can be corrected the day a key is lost; a compiled-in
|
||||
array can only be corrected by shipping a build.
|
||||
- **Consider co-signing for the default set alone.** Those extensions reach
|
||||
every fresh profile without anyone choosing them, which makes a single
|
||||
compromised key worst exactly there. Requiring two signatures is a bounded
|
||||
amount of work when it applies to a handful of ids rather than to the
|
||||
whole catalogue.
|
||||
|
||||
The narrowing itself is small — `verifySignature` and its two callers. The
|
||||
work is in deciding where the binding lives and showing it honestly in
|
||||
Settings, so "who is allowed to update this" is something a user can read
|
||||
rather than infer.
|
||||
|
||||
## The one thing that needs care
|
||||
|
||||
If defaults are fetched from the catalogue on first run, a fresh install with
|
||||
|
|
@ -66,7 +109,9 @@ everywhere else.
|
|||
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.
|
||||
shapes; an extension has a publisher and an updates URL. `verifySignature`
|
||||
takes the keys allowed for *that* id rather than the global array, and
|
||||
`pickChannelEntry` resolves the binding before it verifies anything.
|
||||
- **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
|
||||
|
|
@ -85,3 +130,15 @@ everywhere else.
|
|||
- **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.
|
||||
- **Where does a per-extension key binding live?** In the manifest, where
|
||||
the extension states its own publisher and an attacker who can rewrite the
|
||||
manifest can restate it; in the app's config, where it is trustworthy but
|
||||
only changes with a release; or on chain, where the name already answers
|
||||
the question. Probably: on chain for the catalogue, pinned in config for
|
||||
the default set, and never simply believed from the manifest.
|
||||
- **What happens to an extension whose signer is no longer allowed?** It
|
||||
should stop updating, but stopping it from *running* is a different and
|
||||
much ruder decision, and the two shouldn't be conflated by accident.
|
||||
- **Does co-signing need a threshold, or just two named keys?** Two keys is
|
||||
simple to implement and to explain; m-of-n is where this quietly turns
|
||||
into key management.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.51",
|
||||
"version": "0.3.52",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
"main": "main.js",
|
||||
|
|
|
|||