Skip to content

Two-task nav, key generation out of the app, and three CSP/theme faults - #8

Merged
0c-coder merged 5 commits into
heroku-deployfrom
fix/drop-req-press-opcodes
Sep 18, 2026
Merged

0c-coder merged 5 commits into
heroku-deployfrom
fix/drop-req-press-opcodes

Conversation

@0c-coder

Copy link
Copy Markdown
Owner

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.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.

Both combined pages split to match:

age-derive  ->  age-encrypt  (label, plaintext, Encrypt)
                age-decrypt  (label, age file, Decrypt)
pgp-pqc     ->  pqc-encrypt  (recipient key, Encrypt, Sign)
                pqc-decrypt  (public key + slot, Decrypt, Verify)

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 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.

⚠️ /app/age-derive.html and /app/pgp-pqc.html no longer exist — they are age-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 setpqc command, 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.cpp dispatches exactly OKCONNECT, OKPING, OKDECRYPT and OKSIGN; OKSETPRIV has no branch there.

Three CSP/theme faults, all measured on the live site rather than reasoned about

Every inline style attribute in this app is refused. The CSP is style-src 'self' with nonces and no 'unsafe-inline'. It does not block JS writing element.style.x — CSP does not police CSSOM — so the two look identical in code and behave completely differently. Measured before the fix:

Element attribute in markup computed
.oa-cta-row margin-top:26px 8px
.oa-cta-row a.btn max-width:260px none
footer link group display:flex;gap:20px block

The 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 zero style= attributes in any shell or fragment.

The stylesheets had no version in their URL. hash: true stamps the script tag html-webpack-plugin injects; the <link> tags are hand-written, so onlyagent-theme.css never moved. Caught red-handed — after deploying, the browser still computed the old values while fetch() 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.js now content-hashes src/assets/css into a css_v on all four links.

A random tint on every fieldset. encrypt.js, decrypt.js and search.js each ran fieldset.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.css hard-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 -file routes still work, they are just no longer sold as separate tools.

Verified live

Full AGE round trip on onlyagent.app through 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.

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.
@0c-coder
0c-coder merged commit e724f7d into heroku-deploy Sep 18, 2026
3 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.

2 participants