{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/converter-web",
  "slug": "products/converter-web",
  "kind": "capabilities",
  "bucket": "product",
  "title": "@leumas/converter-web — the converter at converter.leumas.tech",
  "name": "@leumas/converter-web",
  "eyebrow": "the converter at converter.leumas.tech",
  "chip": null,
  "summary": "The public converter microservice at converter.leumas.tech — every pure, offline Leumas adapter as a tool anyone can run without an account. The tool list, every form on it and the sitemap are all...",
  "keywords": [
    "converter",
    "base64",
    "snake case",
    "json formatter",
    "resize image online",
    "free online converter",
    "leumas converter",
    "unit conversion"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/converter-web — the converter at `converter.leumas.tech`\n\nEvery pure, offline Leumas adapter as a tool anyone can run — **no account, nothing uploaded,\nnothing stored**. Base64 and hashes, case conversion and slugs, JSON reshaping, markdown and CSV,\nimage resizing, unit conversion, and one date rendered in twenty-five calendars.\n\n## The one thing to know\n\n**There is no tool list in this product.** The page fetches `GET /api/convert` and builds the whole\ndirectory — and every form on it — from what comes back. A pack added to\n`shared/engines/adapters/` that declares `safe: true` appears here on the next page load, with no\nchange to this product and no redeploy of it.\n\nThat is only possible because a tool's schema is now **its own**. Until recently the adapter registry\npublished the *pack-level* input list for every function of a system, so all eighty of `a-text`'s\nverbs described themselves as taking `{text}` and nothing else. A catalogue built from that would\nhave rendered eighty identical one-box forms, and `Truncate` would have had nowhere to put its\nlength. See `@leumas/schemas/adapter-spec` for the tier order that fixed it.\n\n## Why a public lane exists at all\n\n`/api/adapters` is behind `requireAuth` and PassNode metering. That is right for a surface that can\nshell out to ffmpeg and read files off the server — and it is why a public converter could not exist:\nevery tool on it, including `uppercase`, needed an account.\n\n`/api/convert` (`shared/engines/middleware/src/adapters/convertRouter.js`) is a **second, narrower\ndoor onto the same registry**. `/api/adapters` is untouched; nothing about this lane weakens it.\n\nThree things make it safe, and each is load-bearing:\n\n| | |\n|---|---|\n| **An allowlist that defaults to closed** | `spec.safe` is false unless a pack positively said otherwise, so every pack nobody has reviewed is absent *by construction* rather than by anyone remembering to exclude it. A denylist would have been wrong the first time somebody added a pack. |\n| **`untrusted: true` on the call** | The allowlist alone would **not** have made `image` safe: its `image` argument accepts a filesystem path, which is a convenience for an authenticated operator and an arbitrary-file-read for a visitor. `normalizeInput` refuses paths when this flag is set. |\n| **A body cap** | These tools take their payload inline — an image arrives as base64 — so without a cap the cheapest denial of service is one request. |\n\nA tool that is *well described* is not a tool that is *safe*: `a-audio` has a per-function\n`metadata.json` for all thirty-two of its tools and is the best-documented pack in the tree, and it\nis not on this lane. Conflating the two is how an allowlist built from \"has a schema\" would have\nopened ffmpeg to the internet.\n\n## Running it\n\n```bash\npnpm --filter @leumas/converter-web dev      # :5044, proxying /api to the API on :3000\npnpm --filter @leumas/converter-web build    # -> dist/, which the site row serves\npnpm smoke:converter-site                    # the row AND the lane, offline\n```\n\n**Never set `VITE_API_BASE`.** Most of this site is anonymous, so the damage is subtler than on a\nsigned-in surface: the converter would keep working while the signed-in half silently stopped. An\nabsolute API origin makes every call cross-origin, which drops the httpOnly `leumas_session` cookie.\n\n## How the subdomain answers\n\nA platform-owned Imperium site row — `shared/services/index/src/converterSite.js` — provisioned on\nevery API boot. Read `platformSite.js`'s header before changing it: the hosted-not-first-party lane,\nthe deliberately empty `ownerUserId`, the non-pruning upsert and the `static` target are each a trap\nthat has been hit once already, and none of them is visible from the file itself.\n\nThe edge needs nothing. IIS binds `*.leumas.tech` to the Imperium worker, `converter` is not in\n`RESERVED_SUBDOMAINS`, and `createApiProxy` already forwards `/api` — which `/api/convert` is inside —\nto leumas-api on every hosted host. **Do not** add `/convert` to `PROXIED_PREFIXES` to shorten the\nURL: that list applies to every host the worker serves, so claiming it globally would take the path\naway from every customer site on the platform.\n\n## Root-level filenames are shadowed\n\nimperium-server mounts `express.static` over the **apex** leumas-web build before the target\ndispatcher, on every host. So `favicon.ico`, `favicon-32.png`, `favicon-192.png`,\n`apple-touch-icon.png` and `logo.svg` at this site's root are served from leumas.tech's copy instead.\nNothing errors; the wrong logo simply appears in the tab. Every root-level file this product adds is\nnamespaced `converter-`. Hashed `/assets/*` are safe.\n",
  "source": {
    "path": "products/leumas-converter/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 4896,
    "hash": "82e8ab7b5742176b95cf9350341e7ecccefb1fdc"
  },
  "urls": {
    "html": "/p/products/converter-web",
    "json": "/docs/products/converter-web.json",
    "md": "/docs/products/converter-web.md"
  },
  "links": {
    "composes": [
      "pkg:@leumas/theme",
      "pkg:@leumas/ui"
    ],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  },
  "exports": null
}
