Ship the extension as a signed CRX from our own update server
The dashboard-hosting stack from the previous commit is gone; it was answering a question nobody asked. What was actually needed is Chrome's real update channel, which an unpacked extension does not have and cannot be given. So the extension is now signed into a CRX with an update manifest beside it, served from the droplet, and installed on the trading machines by policy (ExtensionInstallForcelist). From there Chrome installs it, updates it on its own schedule, and puts it back if it goes missing. The manifest gained the `key` that fixes the extension id, so an unpacked development load keeps the same identity as the published build. The self-update check now branches on how it was installed: policy installs ask Chrome to check the update server now rather than waiting out its five-hour cycle, and unpacked loads keep the reload-from-disk path. chrome.management .getSelf() tells the two apart and needs no permission to do it. The signing key lives at /opt/autofirmer-updates/key.pem on the droplet and nowhere else. It is the extension's identity: a different key is a different extension, and no browser would take it as an update. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ece6aab9cf
commit
035f93d220
+55
-13
@@ -867,18 +867,47 @@ function compareVersions(a, b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Reload ourselves when the dashboard ships a newer build than the one running.
|
||||
/** How we were installed: 'admin' when Chrome policy pulled us from the update
|
||||
* server, 'development' when somebody loaded the folder unpacked. getSelf is
|
||||
* the one chrome.management call that needs no permission. */
|
||||
async function installType() {
|
||||
try {
|
||||
const self = await chrome.management.getSelf();
|
||||
return self.installType || 'development';
|
||||
} catch {
|
||||
return 'development';
|
||||
}
|
||||
}
|
||||
|
||||
/** chrome.runtime.requestUpdateCheck as a promise; null if the call is not
|
||||
* available at all. */
|
||||
function requestUpdateCheck() {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
chrome.runtime.requestUpdateCheck((result) => {
|
||||
if (chrome.runtime.lastError) return resolve(null);
|
||||
resolve(result?.status || 'no_update');
|
||||
});
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Get onto the newer build the dashboard is advertising.
|
||||
*
|
||||
* Chrome has no update channel for an unpacked extension — it reads the folder
|
||||
* once at startup and never looks again — but chrome.runtime.reload() does
|
||||
* re-read it from disk, and the updater is already pulling new files into that
|
||||
* folder every few minutes. This closes the last manual step between the two.
|
||||
* Two different mechanisms, because there are two ways this extension gets
|
||||
* installed. A policy install has a real update channel: Chrome polls the CRX
|
||||
* on the droplet on its own schedule, and all we add is "check now" so a
|
||||
* release lands in seconds rather than within five hours.
|
||||
*
|
||||
* It can only ever run code that is already on disk, so a dashboard that is
|
||||
* ahead of this machine's pull would have us reload into the same version over
|
||||
* and over. Hence one attempt per version pair: if we come back unchanged, say
|
||||
* so and wait for the pull instead of restarting forever. A dashboard that is
|
||||
* *behind* is left alone entirely — downgrading is not something we can do. */
|
||||
* An unpacked load has no channel at all — Chrome reads the folder once and
|
||||
* never looks again — but chrome.runtime.reload() re-reads it from disk, which
|
||||
* the update check has already pulled into. That can only ever run code that is
|
||||
* already there, so a dashboard ahead of this machine's pull would have us
|
||||
* reload into the same version forever. Hence one attempt per version pair:
|
||||
* come back unchanged and it says so and waits. A dashboard that is *behind* is
|
||||
* left alone either way — downgrading is not something we can do. */
|
||||
async function maybeSelfUpdate(expected) {
|
||||
const running = chrome.runtime.getManifest().version;
|
||||
const { updateAttempt } = await chrome.storage.local.get('updateAttempt');
|
||||
@@ -893,11 +922,24 @@ async function maybeSelfUpdate(expected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await installType()) !== 'development') {
|
||||
const status = await requestUpdateCheck();
|
||||
// Chrome swaps the extension out from under us when the CRX arrives, so
|
||||
// there is nothing to do afterwards but say what was asked for.
|
||||
await setState({
|
||||
error: status === 'throttled'
|
||||
? `Running v${running}, v${expected} is published — Chrome is rate-limiting `
|
||||
+ `update checks and will pick it up shortly.`
|
||||
: `Running v${running}, asked Chrome to fetch v${expected} (${status || 'no answer'}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateAttempt && updateAttempt.expected === expected && updateAttempt.running === running) {
|
||||
await setState({
|
||||
error: `Running v${running}, the dashboard ships v${expected}. Reloaded once `
|
||||
+ `and the files on disk were still the old ones — waiting for the next `
|
||||
+ `update. If this sticks, pull and reload the card in chrome://extensions.`,
|
||||
error: `Running v${running}, the dashboard ships v${expected}. Reloaded once and `
|
||||
+ `the files on disk were still the old ones — waiting for the next pull. `
|
||||
+ `If this sticks, pull and reload the card in chrome://extensions.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"manifest_version": 3,
|
||||
"name": "AutoFirmer Capture",
|
||||
"version": "0.12.0",
|
||||
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxdbOMRR+58a9dHlbIKtlALtS3E4eltBGqfTnUc0lzJuWj5ZA4+5YFSdUnxv5N1ogxwpJUGkUV3MnKJuZnTekCmsNwjCy41a20Xtc7jQLHrIBZkesB6cB+d0l9Vo8M6EC3ZMD8Iv6MdAalU3GZh1Hn9vm24NjJ+xUT1qNIVHEcNUIb9evYobR/XHv+JAp6bOFnaLM+3wpyYKe4efMhsz0F86u9thRNhQnraTqUWfnKNpsHPK2zivaYHrziEEWX7Y3EsMHkf2FRhkUe7IDoxeJM49DBToTo9sAJUkcgLeGCGZLo7H3mcxyn/zERYR+rD3k5xdeBtjoZHXFx2+FanLb9wIDAQAB",
|
||||
"update_url": "https://ext.juicerroom.com/updates.xml",
|
||||
|
||||
"description": "Scrapes the HTML of the firm tab you are viewing and posts it to the AutoFirmer dashboard.",
|
||||
"permissions": ["scripting", "tabs", "storage", "alarms"],
|
||||
"host_permissions": [
|
||||
|
||||
Reference in New Issue
Block a user