Two-task nav, key generation out of the app, and three CSP/theme faults - #8
Merged
Merged
Conversation
The header carried seven links - encrypt, decrypt, encrypt-file,
decrypt-file, search, pgp-pqc, age-derive - which presented four transports
as if they were four destinations. They are not: they are four ways to do
the same two things. The header is now Encrypt | Decrypt | Search, and a
segmented selector inside each page chooses Message, File, PQC-PGP or AGE.
The selector is real hrefs to real routes, not a JS show/hide. pages.js
already takes over every <a> inserted into the DOM, so a click swaps the
view client-side with no page load while the URL still deep-links and the
back button still works - and a tab that JS cannot reach is a tab that
breaks when JS fails. src/plugins/pages/mode-tabs.js renders the strip; it
is plain CommonJS with no loaders because webpack.config.js's
getPagesList() requires the plugin tree under bare Node.
The two combined pages are split to match:
age-derive -> age-encrypt (label, plaintext, Encrypt)
age-decrypt (label, age file, Decrypt)
pgp-pqc -> pqc-encrypt (key block, Encrypt, Sign)
pqc-decrypt (public key + slot, Decrypt, Verify)
Sign sits under Encrypt and Verify under Decrypt, which is the split the
classic PGP pages already use.
Both splits are markup only. Each pair shares ONE init/setup - the same
thing encrypt.js and decrypt.js already do for their -file siblings - so
every crypto handler is byte-for-byte the code the hardware brief
exercised. The handlers bind by id through jQuery and a selector that
matches nothing binds nothing, so each view wires only the controls it has.
Two helpers that read a field the other view does not have needed a `|| ""`
guard; that is the whole behavioural delta.
Nav membership is a build-time property (getPagesList reads the module-level
pagesList before setup ever runs) and routing is a runtime one, so dropping
`icon` from encrypt-file, decrypt-file and the four new entries removes the
header link while leaving the route and its /app/<name>.html intact.
URLS THAT CHANGED: /app/age-derive.html and /app/pgp-pqc.html no longer
exist. They are /app/age-encrypt.html, /app/age-decrypt.html,
/app/pqc-encrypt.html and /app/pqc-decrypt.html now. Test briefs and
bookmarks pointing at the old two want updating.
Three theme gaps fixed while in here, all from before the monochrome pass:
* The challenge-code box was open on every page load, telling the user to
press buttons for a request nobody had made. It carried
style="display:none", and the CSP allows style-src 'self' with nonces and
no 'unsafe-inline', so the attribute was refused. The default moves to
.oa-challenge in the stylesheet. The JS toggle is unaffected - CSP does
not police CSSOM writes, so element.style.display still shows and hides
it - and the box's own colours were a light-theme green on near-white,
invisible on this background.
* The xterm log panel was black-on-white. xterm paints to a canvas, so CSS
cannot reach it; the theme is set at construction and now matches --panel
and --text.
* input[type="number"] was missing from the form-control selector list, so
the RSA-slot field was a white box in a dark form, and tokenizer.css
hard-codes a white wrapper around the recipient field. Both overridden.
Rebuilds docs/ - the onlyagent.app deployment - for the previous commit. /app/age-derive.html and /app/pgp-pqc.html are replaced by age-encrypt, age-decrypt, pqc-encrypt and pqc-decrypt.
Same root cause as the challenge-code box: this app's CSP allows style-src 'self' with nonces and no 'unsafe-inline', so every style="..." attribute in the markup is refused. Verified live rather than assumed - on onlyagent.app, .oa-cta-row carried style="margin-top:26px" and computed 8px, its primary button carried max-width:260px and computed none, and the footer link group carried display:flex and computed block. So three things on the live home page were quietly wrong: the hero CTA row sat at the default spacing, the 'Encrypt a message' button ran the full width of the row instead of capping at 260px, and the footer links stacked instead of sitting in a row. The app shell's footer had the same stacking. Moved to .oa-hero .oa-cta-row, .oa-cta-row .oa-btn-primary and .oa-footer-links. There are now no style= attributes left in any shell or page fragment, so the next one added will be the only one - and it will not work either.
The bundle gets html-webpack-plugin's `hash: true`, which stamps the script tag it injects. The <link> tags are hand-written in the two shell templates, so they got nothing - every page asked for `css/onlyagent-theme.css` at a URL that never moved, and GitHub Pages serves it with a long max-age. Measured on onlyagent.app today: a browser was holding a pre-monochrome stylesheet while the server had the current one. Same page, same session - `fetch()` with a query string returned the right file, the <link> did not. So every CSS change shipped so far, the monochrome theme and today's fixes included, reaches returning visitors only when their cache happens to expire. webpack.config.js now hashes the contents of src/assets/css and passes it to the templates as css_v, appended to all four stylesheet links. Hashing the contents rather than stamping a timestamp means the URL moves only when a stylesheet actually changes, so an unchanged build stays cached. hashDir() is non-fatal - a missing directory yields a constant, which is no worse than the behaviour it replaces. Current value: 1a155b6c1fe6.
…ome cards
Three separate simplifications.
1. COMPOSITE KEY CREATION LEAVES THE WEB APP
Encrypt > PQC-PGP carried a key block - Generate Composite Key, the
assembled `onlykey-cli setpqc` command, a download button for the 160-byte
private blob, and two paragraphs explaining config mode and why the app
cannot load a private key itself. All of it is gone, markup and handlers
both, along with generateCompositeKey()'s only call site.
The page now has the same shape as Encrypt > Message: a recipient's public
key, a slot number naming a key the device already holds, plaintext, and
the Encrypt and Sign buttons.
The reason is not tidiness. Generating a key in a browser puts the private
half in a JS heap in a tab, and the handoff then asks the user to carry it
to the device by hand - through the clipboard, a file on disk, or a command
in shell history. None of those is a place a private key should be, and an
encrypt page had no business explaining any of it. `onlykey-cli loadpqc`
and `setpqc` already do this against config mode over the vendor interface,
which a browser cannot reach.
2. THE RANDOM FIELDSET TINT IS GONE
encrypt.js, decrypt.js and search.js each set
document.getElementsByTagName('fieldset')[0].style.backgroundColor =
app.randomColor({ luminosity: 'bright', format: 'rgba' })
on every render - a different washed-out hue per page and per visit. It
predates the theme and fights it: the panel is a flat near-black by design,
and a translucent colour over it showed as bars wherever the fieldset
peeked out between its children. Olive on encrypt, navy on encrypt-file,
something else tomorrow.
Worth recording how this hid: a CSSOM write is not an inline style, so the
CSP that refuses every style= attribute in this app let this through. I
also looked for it once with a filter that only matched pure green
(g > r+25 && g > b+25), which both olive and navy fail, and wrongly
concluded the bars were a browser-extension overlay. They were always real.
3. THE LANDING PAGE MATCHES THE APP AGAIN
Five tool cards - Encrypt Messages, Encrypt Files, Decrypt Messages,
Decrypt Files, Search - became three: Encrypt & Sign, Decrypt & Verify,
Find a Recipient. A file and a message stopped being separate destinations
when the method selector landed; the home page was still advertising the
old shape. The -file routes still work, they are just not sold as tools.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything here is already live — GitHub Pages serves from
fix/drop-req-press-opcodes/docs, so the pushes released it. This is the integration merge, the same as #7.Two tasks in the header, the method picked inside the page
The header carried seven links, presenting four transports as four destinations. It is now Encrypt | Decrypt | Search, with a segmented Message / File / PQC-PGP / AGE selector inside each page.
The selector is real hrefs to real routes, not a JS show/hide.
pages.jsalready takes over every<a>inserted into the DOM, so a click swaps the view client-side with no page load while the URL still deep-links and the back button still works — and a tab that JS cannot reach is a tab that breaks when JS fails.Both combined pages split to match:
Sign under Encrypt, Verify under Decrypt — the split classic PGP already uses.
The splits are markup only. Each pair shares one
init/setup, the same thingencrypt.jsanddecrypt.jsalready do for their-filesiblings, so every crypto handler is byte-for-byte the code the hardware brief exercised./app/age-derive.htmland/app/pgp-pqc.htmlno longer exist — they areage-encrypt,age-decrypt,pqc-encrypt,pqc-decrypt. The transit-v2 test brief has been updated.Key generation leaves the web app
Encrypt › PQC-PGP carried Generate Composite Key, an assembled
setpqccommand, a download button for the 160-byte private blob, and two paragraphs about config mode. All gone, markup and handlers both.Generating a key in a browser puts the private half in a JS heap in a tab, and the handoff then asks the user to carry it to the device by hand — clipboard, a file on disk, a command in shell history. The CLI does this properly against config mode over the vendor interface, which a browser cannot reach anyway.
Worth recording from the firmware read this prompted: the browser transport cannot write keys at all.
ok_extension.cppdispatches exactlyOKCONNECT,OKPING,OKDECRYPTandOKSIGN;OKSETPRIVhas no branch there.Three CSP/theme faults, all measured on the live site rather than reasoned about
Every inline
styleattribute in this app is refused. The CSP isstyle-src 'self'with nonces and no'unsafe-inline'. It does not block JS writingelement.style.x— CSP does not police CSSOM — so the two look identical in code and behave completely differently. Measured before the fix:.oa-cta-rowmargin-top:26px8px.oa-cta-row a.btnmax-width:260pxnonedisplay:flex;gap:20pxblockThe one that mattered: the derived-decap challenge box carried
style="display:none"and was therefore open on every page load, telling the user to press buttons for a request nobody had made. Its JS toggle worked fine, which is why it went unnoticed. There are now zerostyle=attributes in any shell or fragment.The stylesheets had no version in their URL.
hash: truestamps the script tag html-webpack-plugin injects; the<link>tags are hand-written, soonlyagent-theme.cssnever moved. Caught red-handed — after deploying, the browser still computed the old values whilefetch()with a query string returned a file that did contain the new rules. So every CSS change shipped so far, the monochrome rebrand included, reached returning visitors only when their cache happened to expire.webpack.config.jsnow content-hashessrc/assets/cssinto acss_von all four links.A random tint on every fieldset.
encrypt.js,decrypt.jsandsearch.jseach ranfieldset.style.backgroundColor = app.randomColor({luminosity:'bright'})on every render — olive on one page, navy on the next. It predates the theme and fought it. Removed from all three. (It hid behind the same CSSOM-vs-attribute distinction as the challenge box.)Also: the xterm log panel was black-on-white — it paints to a canvas so CSS cannot reach it, and the theme is set at construction now;
input[type="number"]was missing from the form-control list;tokenizer.csshard-codes a white wrapper on the recipient field.Landing page
Five tool cards became three — Encrypt & Sign, Decrypt & Verify, Find a Recipient — matching the app's header. The
-fileroutes still work, they are just no longer sold as separate tools.Verified live
Full AGE round trip on
onlyagent.appthrough the new split routes: Encrypt › AGE produced a 2264-char age file, Decrypt › AGE returned the exact plaintext. That exercises the derive path, transit v2 framing, the 7-chunk 171-byte reassembly and the sealed 32-byte secret after the reorganisation. The challenge box computed a code and cleared itself on success.One finding from that run: it completed with no button press and no challenge entry, so field 30 is still
2(USER_INPUT_NONE) — never restored after the 2026-09-15 step 7 test.