# @leumas/encrypt-web — the encrypt/decrypt lab at encrypt.leumas.tech

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`, `Disallow`ed. |
| `/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.


---
Source: products/leumas-encrypt/README.md
Canonical: https://docs.leumas.tech/p/products/encrypt-web
