Skip to content

Add tron store so publishing is one command - #127

Merged
ralyodio merged 3 commits into
mainfrom
feat/tron-store-cli
Sep 24, 2026
Merged

ralyodio merged 3 commits into
mainfrom
feat/tron-store-cli

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Add tron store so publishing is one command

The store auto-updates anything listed in it, and the launcher now pulls those
updates, so getting an extension listed is the whole job. Doing that by hand is
exactly why the store held one listing while we shipped five extensions.

tron store login save a publisher token for this machine
tron store whoami which publisher this machine is
tron store list slugs currently in the store
tron store publish --name ... --manifest ... --bundle-url ...

Plain sh and curl on purpose: publishing has to work on a box with no Node and
in a CI image holding nothing but this script. CI sets TRONBROWSER_STORE_TOKEN;
a person runs login once and the token is saved 0600 under XDG_CONFIG_HOME.

login is a paste rather than a flow we complete for them, because the API
refuses to mint a publisher token from a token -- deliberately, so a leaked CI
token cannot mint more. That one step stays human by design, and this is the
shortest honest version of it.

publish finds an existing listing by slug and creates one only when there is
none, so re-running does not leave a second listing beside the first. It derives
the slug the way the server does, and --slug overrides it for a listing whose
slug was customised. A version that is already published exits clean, so
re-running a tag is safe.

install.sh writes this CLI from a quoted heredoc, and nothing in the build parses
what is inside it. Extracted and sh -n'd, then run: help, list against the live
store (returns coinpay-wallet), and publish with no token, which points at
tron store login rather than failing with a 401 from curl.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

ralyodio and others added 3 commits September 24, 2026 12:54
We built a store with one-click install and Chromium auto-update -- crx.ts
stamps our update_url into the manifest before signing, precisely so Chromium
polls /api/store/updates.xml rather than Google. Then every listing was created
by hand, which in practice meant almost none were: the store holds exactly one
extension, and our own MarkSyncr is not in it.

That is the gap worth closing. An extension in the store auto-updates for anyone
who installed it. An extension outside the store reaches TronBrowser only by
being rebuilt into the bundle, and reaches everyone else only when Google
approves it. MarkSyncr has three releases sitting in Chrome review right now.

So publishing runs from CI. scripts/store-publish.mjs resolves the listing by
slug, creates it on first run, and posts the version with the manifest and the
artifact URL. The reusable workflow wraps it so an extension repo adds six lines
rather than its own half of the job.

Two behaviours worth stating. A version that is already published exits clean
instead of failing, so re-running a tag is safe. And the artifact is polled
before submitting, because the store fetches it itself and a release asset can
lag its tag by a few seconds -- submitting into that window fails for a reason
that has nothing to do with the extension.

Verified against the live store: an existing listing resolves by slug
(coinpay-wallet), an unknown name takes the create path, and a manifest that is
not MV3 is refused before anything is sent.

Nothing publishes until TRONBROWSER_STORE_TOKEN exists. The API deliberately
refuses to mint a publisher token from a token -- it has to come from a signed-in
browser session -- so that one step is Anthony's and cannot be automated away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extensions load with --load-extension, and Chromium never auto-updates an
extension loaded that way. That is why a bundled extension could only change when
the whole browser was re-released: v3.15.0 shipped a MarkSyncr vault import bug
that had already been fixed upstream, and there was nothing to do but wait for
the next TronBrowser build.

So the launcher updates them itself, against tronbrowser.dev/store, which has no
review queue in front of it. The store already stamps its own update_url into
each signed manifest for exactly this purpose; what was missing was anything on
this side asking.

The bundled copy is never written to -- it lives in the install tree, which may
be read-only and is replaced wholesale on upgrade. Newer versions land in an
overlay under the data dir, and an extension loads from the overlay only while it
is genuinely newer. When a browser upgrade brings the bundle level with it, the
overlay is deleted rather than left to shadow it forever.

The check runs detached and applies on the NEXT launch. Blocking startup on the
network to save one launch's staleness is a bad trade, and swapping an
extension's files under a running Chromium is worse.

Everything about it fails soft: no curl, no unzip, an unreachable store, an
extension the store has never heard of, a download that is not the extension we
asked for -- each leaves the bundled copy in place. A browser that will not start
because a store was down would be a far worse bug than a stale extension.
TRONBROWSER_NO_EXT_UPDATE=1 turns the whole thing off.

Two things worth keeping in mind. Versions are compared numerically per field,
because a string compare puts 1.2.10 below 1.2.9. And this script runs under
`set -eu`, so _manifest_version returns 0 with no output for a missing file:
the first version of it returned 1, which inside a command substitution would
have taken the browser down on every launch where the overlay did not exist yet.
The functional test caught that before it shipped.

Verified against the live store under set -eu: a staged coinpay-wallet 0.0.1 is
replaced by 0.10.1 and loads from the overlay, an extension the store does not
have is left alone, and the run survives to the end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The store auto-updates anything listed in it, and the launcher now pulls those
updates, so getting an extension listed is the whole job. Doing that by hand is
exactly why the store held one listing while we shipped five extensions.

  tron store login                 save a publisher token for this machine
  tron store whoami                which publisher this machine is
  tron store list                  slugs currently in the store
  tron store publish --name ... --manifest ... --bundle-url ...

Plain sh and curl on purpose: publishing has to work on a box with no Node and
in a CI image holding nothing but this script. CI sets TRONBROWSER_STORE_TOKEN;
a person runs login once and the token is saved 0600 under XDG_CONFIG_HOME.

login is a paste rather than a flow we complete for them, because the API
refuses to mint a publisher token from a token -- deliberately, so a leaked CI
token cannot mint more. That one step stays human by design, and this is the
shortest honest version of it.

publish finds an existing listing by slug and creates one only when there is
none, so re-running does not leave a second listing beside the first. It derives
the slug the way the server does, and --slug overrides it for a listing whose
slug was customised. A version that is already published exits clean, so
re-running a tag is safe.

install.sh writes this CLI from a quoted heredoc, and nothing in the build parses
what is inside it. Extracted and `sh -n`'d, then run: help, list against the live
store (returns coinpay-wallet), and publish with no token, which points at
`tron store login` rather than failing with a 401 from curl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

ThreatCrush Security Scan

52 finding(s)

HIGH/CRITICAL: 3 | MEDIUM: 28 | LOW: 21

Severity Rule Location
HIGH py-ssrf-outbound-request apps/desktop/launcher/tron-tor-helper:303
HIGH py-ssrf-outbound-request apps/desktop/launcher/tron-tor-helper:437
HIGH sh-remote-script-execution apps/web/public/install.sh:879
MEDIUM js-open-redirect apps/desktop/extensions/ai-sidebar/install-helper.js:156
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:34
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:57
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:237
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:266
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:336
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/options.js:306
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:78
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:166
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:77
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:227
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:569
MEDIUM js-unescaped-html-sink apps/web/public/app.js:29
MEDIUM js-unescaped-html-sink apps/web/public/dns.js:54
MEDIUM sh-remote-script-execution apps/web/public/install.sh:164
MEDIUM sh-remote-script-execution apps/web/public/install.sh:169
MEDIUM sh-remote-script-execution apps/web/public/install.sh:404
MEDIUM sh-remote-script-execution apps/web/public/install.sh:422
MEDIUM sh-remote-script-execution apps/web/public/install.sh:935
MEDIUM sh-remote-script-execution apps/web/public/install.sh:986
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:96
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:168
MEDIUM js-dynamic-code-execution packages/sdk/src/mcp/tools.ts:125
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:68
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:73
MEDIUM sql-template-interpolation services/api/src/store/db.ts:116
MEDIUM js-dynamic-code-execution services/api/src/store/scanner.ts:44
MEDIUM sh-predictable-temp-path start.sh:25
LOW js-dynamic-code-execution apps/desktop/extensions/ai-sidebar/pit-proxy.test.js:8
LOW js-dynamic-code-execution packages/agent-runtime/src/analyze/form-script.test.ts:6
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:26
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:49
LOW js-dynamic-code-execution packages/browser-core/src/automation/extract-script.test.ts:6
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:16
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:45
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:57
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:80
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:99
LOW js-dynamic-code-execution packages/browser-core/src/automation/snapshot-script.test.ts:11
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:62
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:101
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:47
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:170
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:216
LOW js-dynamic-code-execution packages/sdk/src/mcp/automate.test.ts:261
LOW secret-generic-api-key packages/storage/src/config.ts:51
LOW secret-generic-credential packages/storage/src/config.ts:51

…and 2 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 9d59b1e into main Sep 24, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant