{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/hosting-upload",
  "slug": "packages/hosting-upload",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/hosting-upload",
  "name": "@leumas/hosting-upload",
  "eyebrow": null,
  "chip": null,
  "summary": "What gets deployed and what never does — the upload rules shared by Studio's drag-and-drop uploader and the agent lane, plus the Node walker and pooled uploader.",
  "keywords": [
    "hosting-upload",
    "drag-and-drop",
    "uploader",
    "deployed",
    "walker",
    "leumas hosting upload",
    "limits respects",
    "contract sharable"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# `@leumas/hosting-upload`\n\n**What gets deployed, and what never does — decided once.**\n\nThe same decisions have to be made in two places that share no runtime: Studio's drag-and-drop\nuploader (a browser holding `File` handles) and the agent lane (a Node process on somebody's laptop\nholding paths). Both must strip the same wrapper folder, skip the same directories and refuse the\nsame files.\n\nThe consequence of them disagreeing is not cosmetic. **`.env` is on the skip list because a bundle\ndirectory is served to the public internet** — a second implementation that forgets it publishes a\nsecret, and does it silently. So the rules live here once, in a module that imports nothing.\n\n## The one contract that made it sharable\n\nAn entry is `{ rel, size, file?, path? }` — **not** `{ rel, file }`. The planner reads `size` and\ncarries everything else through untouched, so a browser passes `{ file, size: file.size, rel }` and\nthe Node walker passes `{ path, size, rel }`. Neither runtime gets to decide policy.\n\n## Exports\n\n| Import | What |\n|---|---|\n| `@leumas/hosting-upload` | the rules. Pure, no `node:` imports, safe in a browser bundle |\n| `@leumas/hosting-upload/node` | `walkDir`, `uploadOne`, `uploadPlan` — real files, real bytes |\n\n`planUpload(entries)` → `{ files, skipped, stripped, buildDir, oversized, bytes, count }`.\n`rerootToBuildDir(plan)` accepts the `buildDir` offer. `looksLikeSpa(files)` answers the one\nquestion that actually breaks deploys in the field.\n\n**The barrel is the rules only, deliberately** — `./node` imports `node:fs`, and a browser bundling\nthe barrel would pull it in.\n\n## Why `looksLikeSpa` exists\n\n`target.spa` rewrites unknown paths to `index.html`. Getting it wrong is *the* failure this feature\nhas in production: the home page is perfect and every deep link 404s, so nothing catches it — not a\nsmoke test, not a glance at the deployed site. It answers yes only to the shape a single-page app\nactually has (one `.html`, at the root), because a multi-page static site with SPA on turns every\n404 into the home page — the opposite failure, and equally silent.\n\n## Why one file per request\n\nThe upload route takes a single file as a raw stream; `express.json()` is deliberately **not**\nmounted on it, which is the only reason a build can travel at all. A base64 tarball through a tool\nargument is capped by the `/mcp` body limit and inflated 4/3 by the encoding on top — this repo has\nalready paid for that once, at 78 MB against a 2 MB limit.\n\n`uploadPlan` therefore runs a bounded pool and reports `ok` only when **every** file landed. A caller\nmust not point `target.dir` at a partially uploaded build: it serves a broken site with no error\nanywhere. Two refusals stop the run early rather than repeating — `storage_full` and a rejected\ncredential both make every remaining request fail identically, and a hundred identical errors buries\nthe one line that says what to do.\n\n## Limits it respects\n\n- **25 MB per file** (`MAX_UPLOAD_BYTES`, mirrors the server, enforced there twice)\n- **Per site**: the owner's `imperium.siteStorageBytes` — 50 MB free, 250 MB on the plan\n- `DENY_EXT` / `DENY_NAME` are a **mirror** of the server's list, checked client-side to fail fast\n  with a better message. Never the guard.\n",
  "source": {
    "path": "shared/packages/hosting-upload/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 3459,
    "hash": "e96d3a6346aad8116cdf645020782ac06508dc82"
  },
  "urls": {
    "html": "/p/packages/hosting-upload",
    "json": "/docs/packages/hosting-upload.json",
    "md": "/docs/packages/hosting-upload.md"
  },
  "links": {
    "composes": [],
    "usedBy": [
      "pkg:@leumas/apps",
      "pkg:@leumas/mcp-bridge",
      "pkg:@leumas/studio"
    ],
    "product": [
      "pkg:@leumas/admin",
      "pkg:@leumas/studio"
    ],
    "howTo": [],
    "skills": []
  },
  "exports": {
    "total": 16,
    "component": 6,
    "hook": 0,
    "helper": 10,
    "names": [
      {
        "n": "BUILD_DIRS",
        "k": "component"
      },
      {
        "n": "DENY_EXT",
        "k": "component"
      },
      {
        "n": "DENY_NAME",
        "k": "component"
      },
      {
        "n": "MAX_UPLOAD_BYTES",
        "k": "component"
      },
      {
        "n": "SKIP_DIRS",
        "k": "component"
      },
      {
        "n": "SKIP_FILE",
        "k": "component"
      },
      {
        "n": "cleanRelPath",
        "k": "helper"
      },
      {
        "n": "deniedReason",
        "k": "helper"
      },
      {
        "n": "looksLikeSpa",
        "k": "helper"
      },
      {
        "n": "newBuildId",
        "k": "helper"
      },
      {
        "n": "planUpload",
        "k": "helper"
      },
      {
        "n": "rerootToBuildDir",
        "k": "helper"
      },
      {
        "n": "skipReason",
        "k": "helper"
      },
      {
        "n": "uploadOne",
        "k": "helper"
      },
      {
        "n": "uploadPlan",
        "k": "helper"
      },
      {
        "n": "walkDir",
        "k": "helper"
      }
    ]
  }
}
