oxedyne/daimond/dev/extdev.mjs
6.6 KiB, 1 run
created by r2519314175:17, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // dev/extdev.mjs — the DEVELOPMENT build of the extension, which is the only one |
| 2 | // that will talk to a page on localhost. |
| 3 | // |
| 4 | // `ext/manifest.json` is the SHIPPED manifest, and it names one origin: |
| 5 | // `https://daimond.oxedyne.com`. It did not always. Until 2026-08-02 it also |
| 6 | // listed `http://127.0.0.1:8777` and `http://localhost:8777`, in |
| 7 | // `externally_connectable` AND in the content-script matches, and both shipped |
| 8 | // to users. A reviewer served a bare hostile HTML page from that port, opened it, |
| 9 | // and completed an exec with a fence of its own choosing. Anything that binds |
| 10 | // 8777 on a user's machine -- a stray dev server, a static server rooted in |
| 11 | // ~/Downloads, another account on a shared box, user-level malware -- was one |
| 12 | // `chrome.runtime.connect` away from running programs as them. |
| 13 | // |
| 14 | // The origins cannot simply be deleted, because local end-to-end testing on |
| 15 | // 127.0.0.1:8777 is how the machine hand is developed. So they live HERE, in a |
| 16 | // generated tree, and never in the file a release is carved from: |
| 17 | // |
| 18 | // node dev/extdev.mjs # build it, print the path |
| 19 | // node dev/extdev.mjs --print # print the path only, build nothing |
| 20 | // |
| 21 | // The generated tree is a COPY of `ext/`, plus a `manifest.json` that is the |
| 22 | // shipped one with the dev origins added back. It was a symlink farm first, |
| 23 | // which would have made every edit live -- but Chrome will not inject a content |
| 24 | // script whose file resolves outside the extension root, so `announce.js` was |
| 25 | // silently never injected and the page could not find the extension at all. A |
| 26 | // copy, rebuilt on every call, is the version that works. |
| 27 | // |
| 28 | // A developer opts in by loading THAT directory from chrome://extensions |
| 29 | // instead of `ext/`. The pinned key is in the manifest, so the extension id is |
| 30 | // the same either way and the native messaging host's `allowed_origins` needs no |
| 31 | // second entry. After editing anything under `ext/`, run this again before |
| 32 | // pressing Reload -- the copy is what the browser is holding. Every harness |
| 33 | // rebuilds it as it launches, so only a hand-loaded browser needs the habit. |
| 34 | // |
| 35 | // A release is `ext/` exactly as it sits in the tree. `dev/publish.mjs` refuses |
| 36 | // to carve a manifest that names a loopback origin, so the dev variant cannot |
| 37 | // reach the public mirror even if someone patches the shipped file by hand. |
| 38 | |
| 39 | import { readdir, readFile, writeFile, mkdir, cp, rm, lstat } from 'node:fs/promises'; |
| 40 | import { join, normalize } from 'node:path'; |
| 41 | import { fileURLToPath } from 'node:url'; |
| 42 | import os from 'node:os'; |
| 43 | |
| 44 | const ROOT = normalize(join(fileURLToPath(import.meta.url), '..', '..')); |
| 45 | const EXT = join(ROOT, 'ext'); |
| 46 | // Not /tmp -- it is a tmpfs, and what is written there is RAM charged to this |
| 47 | // machine's agent fleet. See the SCRATCH note in harness.mjs. |
| 48 | const SCRATCH = process.env.DAIMOND_SCRATCH || join(os.homedir(), '.cache/daimond'); |
| 49 | const OUT = join(SCRATCH, 'ext-dev'); |
| 50 | |
| 51 | /// The port `dev/serve.mjs` binds, and therefore the one the dev build trusts. |
| 52 | /// |
| 53 | /// It follows `DAIMOND_PORT`, so a dev build made inside a numbered world (see |
| 54 | /// `dev/world.sh`) trusts that world's server rather than the default one. |
| 55 | export const DEV_PORT = Number(process.env.DAIMOND_DEV_PORT || process.env.DAIMOND_PORT || 8777); |
| 56 | |
| 57 | /// The origins a developer needs and a user must never have. |
| 58 | /// |
| 59 | /// Chrome matches the origin STRING, so the two spellings of loopback are two |
| 60 | /// origins and both are needed. |
| 61 | /// |
| 62 | /// # Arguments |
| 63 | /// * `port` - Which port, for a test that cannot have the usual one. |
| 64 | export function devOrigins(port = DEV_PORT) { |
| 65 | return [ |
| 66 | `http://127.0.0.1:${port}/*`, |
| 67 | `http://localhost:${port}/*`, |
| 68 | ]; |
| 69 | } |
| 70 | |
| 71 | /// The default pair, for a caller that does not care. |
| 72 | export const DEV_ORIGINS = devOrigins(); |
| 73 | |
| 74 | /// Anything in the shipped manifest that looks like one of these has been |
| 75 | /// patched by hand, and the point of the split has been lost. |
| 76 | export const LOOPBACK = /(^|\/\/)(127\.0\.0\.1|localhost|\[::1\]|0\.0\.0\.0)(:|\/)/; |
| 77 | |
| 78 | /// Reads the shipped manifest, refusing one that already carries a dev origin. |
| 79 | async function shipped() { |
| 80 | const text = await readFile(join(EXT, 'manifest.json'), 'utf8'); |
| 81 | const pats = [].concat( |
| 82 | (JSON.parse(text).externally_connectable || {}).matches || [], |
| 83 | ...(JSON.parse(text).content_scripts || []).map((cs) => cs.matches || [])); |
| 84 | const bad = pats.filter((p) => LOOPBACK.test(p)); |
| 85 | if (bad.length) { |
| 86 | throw new Error(`ext/manifest.json already names ${bad.join(', ')}. That file is what ships: ` |
| 87 | + `take the loopback origins out of it and let this script add them to the dev build instead.`); |
| 88 | } |
| 89 | return JSON.parse(text); |
| 90 | } |
| 91 | |
| 92 | /// Builds the dev tree and returns its path. |
| 93 | /// |
| 94 | /// # Arguments |
| 95 | /// * `port` - The dev server's port. A test that cannot have the world's -- because |
| 96 | /// a developer is already serving on it -- gets a build of its own rather than |
| 97 | /// fighting for the port. |
| 98 | /// |
| 99 | /// # Returns |
| 100 | /// The absolute path to load unpacked. |
| 101 | export async function extDev(port = DEV_PORT) { |
| 102 | const m = await shipped(); |
| 103 | const origins = devOrigins(port); |
| 104 | const out = port === DEV_PORT ? OUT : `${OUT}-${port}`; |
| 105 | |
| 106 | m.externally_connectable = m.externally_connectable || { matches: [] }; |
| 107 | m.externally_connectable.matches = m.externally_connectable.matches.concat(origins); |
| 108 | for (const cs of m.content_scripts || []) { |
| 109 | cs.matches = (cs.matches || []).concat(origins); |
| 110 | } |
| 111 | // Nothing else is touched. `name`, `version` and the pinned `key` are the |
| 112 | // shipped ones, so the extension id is the same in both builds and the |
| 113 | // native messaging host's allowed_origins needs no second entry -- and a |
| 114 | // test that reads the manifest reads what a user would. |
| 115 | await rm(out, { recursive: true, force: true }); |
| 116 | await mkdir(out, { recursive: true }); |
| 117 | for (const ent of await readdir(EXT, { withFileTypes: true })) { |
| 118 | if (ent.name === 'manifest.json') { continue; } |
| 119 | await cp(join(EXT, ent.name), join(out, ent.name), { recursive: true }); |
| 120 | } |
| 121 | await writeFile(join(out, 'manifest.json'), JSON.stringify(m, null, '\t') + '\n'); |
| 122 | return out; |
| 123 | } |
| 124 | |
| 125 | /// Whether a path is the extension source directory, so a caller that asked for |
| 126 | /// `ext/` can be handed the dev build instead. |
| 127 | /// |
| 128 | /// # Arguments |
| 129 | /// * `p` - The path a test asked to load. |
| 130 | export function isExtSource(p) { |
| 131 | return !!p && normalize(String(p)).replace(/\/+$/, '') === EXT; |
| 132 | } |
| 133 | |
| 134 | if (process.argv[1] && fileURLToPath(import.meta.url) === normalize(process.argv[1])) { |
| 135 | if (process.argv.includes('--print')) { |
| 136 | console.log(OUT); |
| 137 | } else { |
| 138 | const where = await extDev(); |
| 139 | await lstat(join(where, 'manifest.json')); |
| 140 | console.log(where); |
| 141 | console.error(`Load unpacked from that directory, not from ext/. It adds ${DEV_ORIGINS.join(' and ')} ` |
| 142 | + `to the shipped manifest and copies everything else. Run this again after editing ext/, then Reload.`); |
| 143 | } |
| 144 | } |