@leumas/encrypt-web
encrypt.leumas.tech — the Leumas encrypt/decrypt lab. A public, prerendered reference to applied cryptography with a page per cipher suite and per KDF, and behind a Leumas membership, a playground...
@leumas/encrypt-web — the encrypt/decrypt lab at encrypt.leumas.tech
A public, prerendered reference to applied cryptography — a page per cipher suite and per key-derivation function — and behind a Leumas membership, a playground where every parameter of an encrypt/decrypt round trip is yours to change and the result is downloadable.
The default lane runs in the visitor's own browser over the Web Crypto API. The file and the passphrase never leave the device, there is no upload and no size limit. A server lane exists for the four things a browser genuinely cannot do.
pnpm dev:encrypt # vite on :5049, API proxied from :3000
pnpm --filter @leumas/encrypt-web build
pnpm smoke:encrypt-browser # the LOCAL lane, in a real browser (needs dev:encrypt, no API)
pnpm smoke:encrypt-site # the build, served through the real Imperium dispatcher
pnpm smoke:encrypt-api # /api/encrypt, gate included, over real HTTP
pnpm check:encrypt-public # every page indexed or disallowed on purpose
The shape
/ | The scroll story, then the catalogue. Public, prerendered, indexed. |
/algorithms | The index — ciphers, KDFs, envelopes, and the classical teaching tier. |
/algorithms/<id> | One page per suite, KDF, envelope and classical cipher — derived from @leumas/crypt's catalogue, so the engine and the sitemap cannot disagree. |
/lab | The playground — three benches. Members only, noindex, Disallowed. |
/login | Sign-in. noindex. |
The three benches
| Bench | What it does |
|---|---|
| Encrypt & decrypt | A file or a message, both directions. Every cipher, KDF and cost knob. The message lane armours to Base64 both ways, so a round trip is copy-then-paste rather than download-then-upload — the half a file-only tool cannot do. |
| Keys & envelopes | Generate an RSA-2048 or P-256 keypair in the tab, seal a file to a public key, open it with the private one. No passphrase anywhere. This is how you encrypt something for somebody else. |
| Classical | Caesar, ROT13, Atbash, Vigenère — live, and a breaker that takes a Caesar apart with no key and shows all 26 candidates. Not encryption, and every surface says so. |
A status rail sits above all three and answers the question the product turns on before you act on it: which computer is about to process this? It also shows the cipher and derivation actually in force — read from the FILE on a decrypt, not from the pickers.
The engine is @leumas/crypt (shared/packages/crypt), the API is **/api/encrypt/* (products/leumas-api/src/routes/encrypt.js), and the subdomain is one configs row written at boot by shared/services/index/src/encryptSite.js.
Six things that will bite whoever edits this next
1. [critical] Never set VITE_API_BASE. Every path is relative and same-origin through Imperium's createApiProxy. An absolute API origin silently drops the httpOnly leumas_session cookie and the page renders permanently signed out, with nothing in the console.
2. [critical] With spa: true, any unproxied path answers index.html at 200 text/html. Every helper in src/lib/api.js returns { status, body, json } and every caller must check json before believing a 200. A caller reading the status alone sees success with a truthy body and concludes the visitor is signed in.
3. [critical] ready is a separate flag from signedIn. For the first ~100ms of every page load the account probe has not answered, so every visitor looks signed out. Gate.jsx renders nothing at all until ready — walling on !signedIn flashes "sign in" at a paying member on every navigation.
4. [critical] three reaches the bundle only through the lazy() in App.jsx, and the crypto lanes only through lib/lab.js's dynamic import. A static import of either puts hundreds of kilobytes on the first paint of every prerendered reference page. check:encrypt-public reads the built chunks and refuses it.
5. [critical] The build must emit sitemap.xml, robots.txt and llms.txt into dist/. servesOwnSeo defers to a site's own copies only when they exist on disk; without them the platform generates a sitemap from the config row, which knows no routes, and silently publishes one URL.
6. [critical] Namespace every root asset encrypt-. imperium-server mounts the apex leumas-web build's express.static before this site's dispatcher on every host, so favicon.ico, favicon-32.png, favicon-192.png, apple-touch-icon.png and logo.svg are shadowed by leumas.tech's copies. Nothing errors; the wrong logo simply appears in the tab.
The gate, and what it actually proves
Server-side: requireAuth then entitlements.requireCapability('encrypt.run'), applied to the whole router so a route added later is gated by default. Every paid plan grants encrypt.run (MEMBER_CAPABILITIES in @leumas/entitlements's policy.js) and an admin bypasses — that is the owner's rule, "any Leumas membership or an admin", on the sanctioned axis. It is not requireMembership('encrypt'), which would match only a membership whose service was literally encrypt and therefore refuse every existing member.
[warning] The honest limit, which the site itself states. A hosted site's dist/ is world-readable by curl, so a determined non-member who reads the bundle could run the local lane offline. The gate is genuinely enforced on the server lane, on the algorithm catalogue and on anything that persists. Nobody should be told otherwise.
Where the words live
src/content/pages.js and src/content/story.js — as data, not JSX, because scripts/prerender.mjs is plain Node and writes the same sentences into the static HTML that React renders. The home page's copy exists only in story.js, so a prerenderer that lost the beats would ship the page with the most to say as the emptiest document on the site — and it would look completely fine in a browser. check:encrypt-public asserts every beat's words reach the rendered body.
No count is typed anywhere. Every number on the home page interpolates from @leumas/crypt's catalogue, so the page cannot claim a vocabulary the engine does not have.
Guards
smoke:encrypt-browser is the one worth knowing about. The Node suite runs the web lane under Node's crypto.subtle — a real WebCrypto, but not a browser's — and smoke:encrypt-api never touches the local lane at all. So the lane the whole promise rests on was exercised by nothing that runs in Chrome. That guard drives src/lib/lab.js, the exact module Lab.jsx imports, served by vite, inside a real browser: every suite × KDF the catalogue claims for the web lane, the armour carry, both envelopes, and every refusal.
Related
shared/packages/crypt— the engine: the LEUC1 container, the catalogue, and the two lanes.shared/engines/adapters/domain/crypto— the same engine as an adapter, so agents reach it too.products/leumas-programming— the skeleton this was copied from.