Skip to content

JWT signature verification, slug collisions, URL table, output cap - #22

Merged
fadeltd merged 11 commits into
mainfrom
feat/batch-1-followups
Oct 29, 2026
Merged

fadeltd merged 11 commits into
mainfrom
feat/batch-1-followups

Conversation

@fadeltd

@fadeltd fadeltd commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Added

  • JWT Decoder: verify signatures with a PEM, JWK, JWKS or HMAC secret (HS, RS, PS, ES, EdDSA); the key is never saved
  • Slug Generator: flags filenames that collide by case or are duplicated
  • Text to Binary / Hex: Download button; output over 1 MB shows only its start on screen
  • Number Converter: accessible names on the permission checkboxes

Changed

  • The tool request form lists fetching a JWKS by URL as out of scope

Fixed

  • URL Encode / Decode: every query parameter is reachable (previously capped at 300)

Rewrites the 55 classes the Tailwind editor extension flagged as having a canonical spelling (w-[220px] → w-55, rounded-[2px] → rounded-xs, bg-[length:…] → bg-size-[…], [scrollbar-width:none] → scrollbar-none, [&>*]: → *:, and so on). Computed styles are identical before and after on 14 routes at 1280px and 400px.

scripts/tailwind.ts runs Tailwind's own canonicalizer (the engine behind the editor hints) over every string and template literal in src/**/*.tsx, parsed with oxc so JSX text is never touched. `pnpm tw:fix` reports them and `--write` rewrites them; scripts/tailwind.test.ts fails CI when one appears, so new code, including agent-written tools, stays canonical.

Adds @tailwindcss/node 4.3.3 as a dev dependency for its design-system loader.
Map.groupBy is ES2024 and the repo targets the ES2023 lib, so the script failed typecheck.
The test generated two ULIDs with the same seed but the real clock, so a millisecond tick between the calls changed the time prefix and lower !== upper.toLowerCase(). Both calls now use the fixed clock the rest of the file uses.
…oxes

The permission grid shows bare r / w / x, so screen readers announced each checkbox as a single letter. Toggle now takes an aria-label, and each grid checkbox is named for its row and column ("Owner read").
The JWT tool verifies against a pasted JWKS; fetching one by URL would need the CSP's connect-src opened to third parties.
In filename mode, safeFilenameLines now returns collisions: groups of 1-based line numbers whose cleaned names are identical (they collide everywhere), and, separately, groups whose names differ only by case (they collide on Windows and on a default macOS volume). Names are compared after NFC with toLowerCase; a comment explains why not full case folding.

The tool shows each kind as a warning under the summary, e.g. "2 names collide on Windows/macOS: lines 3, 7", listing at most a dozen line numbers.
The parameter table stopped at 300 rows with a "first 300" note. It is now a virtualised list (@tanstack/react-virtual), so every parameter is reachable, each with its own Copy button, raw-vs-decoded display and malformed badge as before.

Rows are measured as they render, since long values wrap and raw forms add a line. The URL parts above share the scroll, so a ResizeObserver keeps the list's scroll margin in step as they change height. The table is a role="table" grid of divs, because absolutely positioned rows cannot be <tr>s.

Checked by hand with a generated 5,000-parameter URL: the last row is reachable and rows never overlap.
Binary output is about 9 characters per input byte, so a few-MB paste put tens of MB in the output textarea. Once the output passes INLINE_MAX_BYTES (1 MB) of characters, in either direction, the textarea shows only the first 64K characters, with "Showing the first N of M characters. Copy and Download include all of it." under it.

Copy still copies the whole output from the click handler, and a new Download button saves it all as a .txt (bytes.<format>.txt or text.txt). Swap is disabled while the output is capped, because it would feed the whole output back into the input textarea.

Checked in a production build: a 3 MB multi-line text encodes to 28 M characters and renders in about 0.6 s; a 1.2 M-character decode in about 0.4 s.
Add verifyJwt(token, key, options) to @fadeltd/devtools-core/jwt. The key can be a JWK, a JWKS (picked by the header kid, or every compatible key tried when there is none), a PEM public key, a PKCS#1 RSA PUBLIC KEY, an X.509 certificate, or an HS secret as text, base64 or base64url. HS, RS, PS and ES at 256/384/512 and EdDSA (Ed25519) are verified with WebCrypto over the original header and payload segments, never re-encoded.

It refuses alg none, algorithm confusion (an HMAC alg with a public key), a curve or key type that does not fit the alg, and a JWK whose alg, use or key_ops conflicts with the header. A DER-encoded ECDSA signature is reported as such. Every failure comes back as a result with a reason; nothing throws.

The JWT Decoder gains a Verify signature panel: a key box with format detection, an encoding select for secrets, and a Signature valid / invalid badge kept separate from the expiry badge. The key is held in memory only, like the token. Fetching a JWKS URL is not offered, because the CSP's connect-src 'self' blocks it, and the panel says so.
Documents JWT signature verification, slug case collisions, the virtualised URL table and the Text to Binary / Hex output cap in both changelogs and READMEs, and updates the JWT blurb and search keywords to mention verification.
@fadeltd
fadeltd merged commit b9f20db into main Oct 29, 2026
1 check passed
@fadeltd
fadeltd deleted the feat/batch-1-followups branch October 29, 2026 17:48
@fadeltd fadeltd changed the title Batch-1 follow-ups: JWT signature verification, slug collisions, URL table, output cap JWT signature verification, slug collisions, URL table, output cap Oct 30, 2026
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