Metadata-Version: 2.4
Name: htmlship
Version: 0.3.6
Summary: Host and share HTML pages from LLMs and coding agents in one line.
Project-URL: Homepage, https://htmlship.com
Project-URL: Repository, https://github.com/htmlship/htmlship
Author: HTMLShip
License: MIT
License-File: LICENSE
Keywords: agent,hosting,html,llm,mcp,paste
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.12
Requires-Dist: click<9,>=8.1
Requires-Dist: httpx<0.28,>=0.27
Provides-Extra: cli
Requires-Dist: pyperclip<2,>=1.9; extra == 'cli'
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.22.1; extra == 'dev'
Requires-Dist: greenlet>=3.5.0; extra == 'dev'
Requires-Dist: pytest-asyncio<0.25,>=0.24; extra == 'dev'
Requires-Dist: pytest-cov<6,>=5; extra == 'dev'
Requires-Dist: pytest<9,>=8; extra == 'dev'
Requires-Dist: ruff<0.8,>=0.7; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.0; extra == 'mcp'
Provides-Extra: server
Requires-Dist: alembic<2,>=1.13; extra == 'server'
Requires-Dist: argon2-cffi<24,>=23; extra == 'server'
Requires-Dist: asyncpg<0.31,>=0.30; extra == 'server'
Requires-Dist: boto3<2,>=1.35; extra == 'server'
Requires-Dist: fastapi<0.116,>=0.115; extra == 'server'
Requires-Dist: itsdangerous<3,>=2.2; extra == 'server'
Requires-Dist: nanoid<3,>=2; extra == 'server'
Requires-Dist: pydantic-settings<3,>=2.5; extra == 'server'
Requires-Dist: pydantic<3,>=2; extra == 'server'
Requires-Dist: python-multipart<0.1,>=0.0.12; extra == 'server'
Requires-Dist: slowapi<0.2,>=0.1.9; extra == 'server'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'server'
Requires-Dist: uvicorn[standard]<0.33,>=0.32; extra == 'server'
Description-Content-Type: text/markdown

# HTMLShip

Host and share HTML pages from LLMs and coding agents in one line.

HTMLShip has four surfaces:

- a public Python library and CLI (`htmlship` on PyPI)
- a Node.js CLI and MCP server (`htmlship` on npm — no install required, runs via `npx`)
- a FastAPI service for creating, updating, deleting, and viewing HTML pages
- a stdio MCP server (the `mcp` subcommand of both CLIs) for agent clients

```bash
# Node — runs immediately, no install
npx htmlship publish report.html
npx htmlship publish report.html --password "demo-pass"

# Build a frontend project (React/Vite/etc.) and ship the compiled app
npx htmlship deploy ./my-app

# Python
pip install htmlship
```

```python
import htmlship

page = htmlship.publish(
    "<h1>Hello</h1>",
    title="Demo",
    password="demo-pass",
    expires_in=60,  # minutes
)
print(page.url)
print(page.owner_key)  # save this to update or delete the page later
```

```bash
curl -X POST https://api.htmlship.com/api/v1/pages \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Hello</h1>","title":"Demo","password":"demo-pass"}'
```

See [`htmlship-implementation-spec.md`](./htmlship-implementation-spec.md) for the product spec and [`DEPLOY.md`](./DEPLOY.md) for the production runbook.

## Python Library

The module-level helpers use `https://api.htmlship.com` by default. Override with `HTMLSHIP_API_URL` or `htmlship.configure(base_url=...)`.

```python
import htmlship

page = htmlship.publish(
    "<h1>Hello</h1>",
    title="Demo",
    password="optional-password",
    expires_in=1440,  # minutes (24 hours)
)

fresh = htmlship.get(page.slug)
updated = htmlship.update(page.slug, "<h1>Updated</h1>", owner_key=page.owner_key)
htmlship.delete(updated.slug, owner_key=page.owner_key)
```

`owner_key` is returned only when a page is created. It is the publisher-only secret required for updates and deletes, and the API does not return it again from metadata calls. `password` is only a view-time gate for readers; it does not authorize mutations.

## CLI

The CLI is shipped both as a Python package (`pip install htmlship`) and an npm package (`npx htmlship` or `npm i -g htmlship`). The two share the same on-disk owner-key store, so a page created in one is editable from the other.

```bash
htmlship publish report.html
cat report.html | htmlship publish -
htmlship publish report.html --password "demo-pass"
htmlship publish --file report.html --title "Q4 Report" --expires-in 60

htmlship get <slug>
htmlship update <slug> updated.html
htmlship delete <slug>
htmlship list-mine
```

Equivalent npx form (no install):

```bash
npx htmlship publish report.html
npx htmlship publish report.html --password "demo-pass"
npx htmlship list-mine
```

The CLI stores owner keys in `~/.htmlship/keys.json` so `update`, `delete`, and `list-mine` can work with pages you created locally. When a page's key isn't saved there and `--owner-key` isn't passed, `update` and `delete` prompt for it interactively — `delete` asks for the key right after the `[y/N]` confirmation — so the public slug alone is never enough to mutate a page. Set `HTMLSHIP_KEYS_DIR` to use another key-store directory, and set `HTMLSHIP_API_URL` to point the CLI at a local or staging API.

## Deploy built apps

`deploy` builds a modern frontend project (React, Vite, Svelte, Next.js, …) **locally** and publishes the compiled output. Single-page apps are inlined into one self-contained page; multi-file sites (Next.js static export — auto-detected — Astro, Docusaurus, VitePress, …) are hosted as a file tree at `view.htmlship.com/{slug}/`. No separate asset hosting required either way.

```bash
htmlship deploy ./my-app                         # SPA -> one inlined page
htmlship deploy ./my-next-app                    # Next.js -> multi-file site (auto-detected)
htmlship deploy ./my-app --site --out dist       # force multi-file hosting (Astro, etc.)
htmlship deploy ./my-app --single-file           # force single-file inlining
htmlship deploy ./my-app --password "demo-pass"  # password-protect the deploy
htmlship deploy --dry-run                         # build, but don't publish (inspect first)
htmlship deploy --build-cmd "vite build" --out dist
npx htmlship deploy ./my-app                      # same, no install
```

How it works:

1. **The build runs locally, on your machine — never on the server.** `deploy` detects your package manager (npm/pnpm/yarn/bun) and runs the `build` script (falling back to `build:prod`, or `--build-cmd` to override). This is what keeps the service from ever executing untrusted build steps.
2. **Single-file** (default for SPAs): the output (`dist/`, `build/`, or `--out`) is inlined into one HTML file — scripts and stylesheets embedded, images/icons/fonts as data URIs — and published with `sandbox_mode: "relaxed"`.
3. **Multi-file** (Next.js auto-detected, or `--site`): the whole output tree is uploaded as a base64 file manifest to `POST /api/v1/sites` and served at `view.htmlship.com/{slug}/`, each response carrying a path-scoped per-slug sandbox CSP.

Both kinds run with relaxed sandboxing (see [Rendering](#rendering)); a single inlined page is capped at 10 MB, a multi-file site at 50 MB. `--single-file` and `--site` force a mode when you don't want the auto-detected one.

The same flow is available programmatically (Python library) and via the `deploy_project` MCP tool:

```python
import htmlship

# build ./my-app locally and publish (auto-detects single-file vs. multi-file site)
page = htmlship.HTMLShipClient().deploy_project("./my-app", password="demo-pass", expires_in=1440)
print(page.url)
```

**Relaxed-mode limits (by design):** a deployed app runs in an isolated, opaque origin and **cannot** make network requests (`connect-src 'none'`), read cookies, access other pages, or use `eval`. For a multi-file Next.js site that means client-side data fetches fail closed and Next falls back to full-page navigation — links still work, runtime API calls don't. It's intended for self-contained client-side apps, demos, and static content — not for apps that call external APIs at runtime.

**What `deploy` supports:**

- **Single-page apps** that build to one `index.html` plus assets — Vite, Create React App, SvelteKit (`adapter-static`), Vue CLI, plain static-site generators. Auto-detects `dist/`, `build/`, and `out/` with no flags.
- **Next.js** (auto-detected): the CLI transparently builds a **static export based at the slug path** — you don't edit `next.config` (JS, `.mjs`, or `.ts` configs are all wrapped automatically and restored afterward). The app must be statically exportable (no middleware, SSR, or server features).
- **Other multi-file frameworks** via `--site` (Astro, Docusaurus, VitePress, multi-page builds): build with your framework's base path set to `/__htmlship_base__` (e.g. Astro's `base`, or `vite build --base=/__htmlship_base__/`) so root-absolute asset URLs resolve under `/{slug}/`; the server rewrites that placeholder to the real slug at upload. Automatic base injection is currently Next.js-only.

## API

Base URL: `https://api.htmlship.com`.

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/health` | Health check with service version. |
| `GET` | `/version` | Service version. |
| `POST` | `/api/v1/pages` | Create a page. |
| `POST` | `/api/v1/sites` | Upload a multi-file static site (base64 file manifest); served at `view.htmlship.com/{slug}/`. |
| `GET` | `/api/v1/pages/{slug}` | Fetch page metadata. |
| `PATCH` | `/api/v1/pages/{slug}` | Replace HTML or title. Requires `X-Owner-Key`. |
| `DELETE` | `/api/v1/pages/{slug}` | Soft-delete a page. Requires `X-Owner-Key`. |
| `POST` | `/api/v1/pages/{slug}/version` | Create a new page linked to an existing parent slug. |

Create payload:

```json
{
  "html": "<h1>Hello</h1>",
  "title": "Optional title",
  "password": "optional password",
  "expires_in": 60,
  "parent_slug": "optional-parent",
  "sandbox_mode": "strict"
}
```

Notes:

- `html` is stored and served verbatim. Scripts are blocked by the view CSP, not by sanitizing the body.
- Payloads are limited to 10 MiB by default.
- `expires_in` is in **minutes** and must be between 1 and 4320 (3 days). Requests above the cap are rejected with `422`. If omitted, the server applies its default TTL (`DEFAULT_EXPIRES_IN_MINUTES`, 60 minutes). The same rules apply to multi-file sites (`POST /api/v1/sites`). Expired uploads stop serving immediately and their stored bytes are reclaimed by [`htmlship-purge`](#expiry-and-cleanup).
- `sandbox_mode` accepts `strict` (default) or `relaxed`. `strict` blocks all scripts; `relaxed` lets the page's own inline scripts run inside an isolated, opaque origin (used by `deploy` for compiled apps). See [Rendering](#rendering).
- Password-protected views set a signed, HTTP-only session cookie after the correct password is submitted.

There is no server-side build endpoint, by design: builds always run on the client (see [Deploy built apps](#deploy-built-apps)). The API only ever receives already-built output — single-file deploys are a `POST /api/v1/pages` with `"sandbox_mode": "relaxed"`; multi-file sites are a `POST /api/v1/sites` with a base64 file manifest, served per-slug at `view.htmlship.com/{slug}/`.

## Rendering

Rendered HTML is served from `view.htmlship.com/{slug}`. **Strict** pages (the default) get a CSP that blocks all scripts:

- `Content-Security-Policy: default-src 'none'`
- images from `data:` and `https:`
- inline styles plus HTTPS stylesheets
- HTTPS/data fonts and HTTPS media
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: no-referrer`
- restrictive `Permissions-Policy`

**Relaxed** pages (`sandbox_mode: "relaxed"`, used by `deploy`) let the page's own inline scripts run but isolate them with `Content-Security-Policy: sandbox allow-scripts` — deliberately *without* `allow-same-origin`. That forces an opaque origin, so a deployed app cannot read cookies, touch `localStorage`, or fetch other pages same-origin. `connect-src 'none'` additionally blocks network egress. The body is still served verbatim; isolation is enforced entirely by response headers, with no change to the single-file storage model.

**Multi-file sites** (`deploy` of a Next.js export or `--site`) are served at `view.htmlship.com/{slug}/` from a per-slug file tree. Each response carries the same opaque-origin `sandbox` CSP, additionally path-scoped to `/{slug}/` (`script-src`/`style-src`/`img-src`/`font-src` are pinned to that prefix). Per-slug isolation therefore comes from the CSP, not from the URL path — one slug's scripts can't read another's, and `connect-src 'none'` blocks egress just as for single-file pages.

The app routes by `Host` header:

- `htmlship.com` serves the landing page
- `api.htmlship.com` serves the API and landing assets
- `view.htmlship.com/{slug}` serves a sandboxed single page; `view.htmlship.com/{slug}/…` serves a multi-file site's tree

For local development without DNS, append `?_host=view.htmlship.com` (or your configured view host) to spoof the host header, for example:

```bash
curl "http://localhost:8000/<slug>?_host=view.htmlship.com"
```

## MCP Server

HTMLShip ships a stdio MCP server with four tools:

- `publish_html` (accepts optional `password`)
- `deploy_project` — build a local frontend project and publish the compiled app (runs the build on the machine hosting the MCP server)
- `fetch_html`
- `update_html`

There are two equivalent ways to run it. **The npx form is the easiest** — it requires no install on the user's machine and works with every MCP client.

### Option A — npx (recommended)

```json
{
  "mcpServers": {
    "htmlship": {
      "command": "npx",
      "args": ["-y", "htmlship", "mcp"],
      "env": {
        "HTMLSHIP_API_URL": "https://api.htmlship.com"
      }
    }
  }
}
```

### Option B — Python install

If you already have a Python install with `pip install htmlship`:

```json
{
  "mcpServers": {
    "htmlship": {
      "command": "htmlship-mcp",
      "env": {
        "HTMLSHIP_API_URL": "https://api.htmlship.com"
      }
    }
  }
}
```

### Claude Desktop

Edit `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Paste either config block above into the `mcpServers` map and restart Claude Desktop. Then ask: `publish this HTML: <h1>test</h1>`.

### Claude Code

Edit `~/.claude.json` or use `claude mcp add` with the same config (set `"type": "stdio"` if you're editing the file by hand).

## Local Development

Requirements:

- Python 3.12+
- [uv](https://docs.astral.sh/uv/)
- Docker

```bash
# 1. Install dependencies (creates .venv)
uv sync --extra server --extra cli --extra mcp --extra dev

# 2. Start Postgres on localhost:5433
docker compose up -d postgres

# 3. Copy env and edit if needed
cp .env.example .env

# 4. Run migrations
uv run alembic upgrade head

# 5. Start the server
uv run uvicorn htmlship_server.main:app --reload

# 6. Health check
curl http://localhost:8000/health
```

Run tests and linting:

```bash
uv run pytest
uv run ruff check .
```

The test suite uses SQLite and a temporary local blob store. Local development uses Postgres metadata plus `./tmp/blobs/` for HTML blobs unless `SPACES_BUCKET` is configured.

## Configuration

The server reads `.env` via Pydantic settings.

| Variable | Default | Purpose |
| --- | --- | --- |
| `DATABASE_URL` | `postgresql+asyncpg://htmlship:htmlship@localhost:5433/htmlship` | Async SQLAlchemy database URL. |
| `PUBLIC_BASE_DOMAIN` | `htmlship.com` | Base domain used to derive host routing. |
| `API_BASE_URL` | `https://api.htmlship.com` | Public API URL setting. |
| `VIEW_BASE_URL` | `https://view.htmlship.com` | Public view URL used in page responses. |
| `LANDING_BASE_URL` | `https://htmlship.com` | Public landing URL. |
| `SPACES_BUCKET` | empty | If empty, use local blob storage; otherwise use DigitalOcean Spaces/S3. |
| `SPACES_REGION` | `nyc3` | Spaces/S3 region. |
| `SPACES_ENDPOINT_URL` | `https://nyc3.digitaloceanspaces.com` | Spaces/S3 endpoint. |
| `SPACES_ACCESS_KEY` / `SPACES_SECRET_KEY` | empty | Spaces/S3 credentials. |
| `SECRET_KEY` | development placeholder | Signs password-view session cookies. Use a strong value in production. |
| `ENVIRONMENT` | `development` | Enables API docs outside production and secure cookies in production. |
| `LOG_LEVEL` | `info` | Application log level. |
| `MAX_PAYLOAD_BYTES` | `10485760` | Server-side single-file HTML size limit (10 MB). |
| `MAX_SITE_BYTES` | `52428800` | Total size limit for a multi-file site upload (50 MB). |
| `MAX_SITE_FILES` | `2000` | Max number of files in a multi-file site upload. |
| `DEFAULT_EXPIRES_IN_MINUTES` | `60` | Default TTL (minutes) applied to new pages and sites when the request omits `expires_in`. Set empty for permanent-by-default. |
| `PURGE_GRACE_MINUTES` | `5` | `htmlship-purge` only removes rows whose expiry/deletion is at least this many minutes in the past. |

### Expiry and cleanup

Every upload gets a TTL (default 60 minutes, capped at 3 days). The view host 404s an expired page the moment its `expires_at` passes; the stored blob is reclaimed separately by a purge command you run on a schedule:

```bash
htmlship-purge              # delete expired/soft-deleted blobs + rows, print a JSON summary
htmlship-purge --dry-run    # report what would be purged, change nothing
```

It removes the backing object for each `page` and the whole key prefix for each multi-file `site`, then hard-deletes the row. A failed blob delete is left for the next run and makes the command exit non-zero. Wire it into cron (see [`deploy/htmlship-purge.cron`](deploy/htmlship-purge.cron)), e.g. every 10 minutes:

```cron
*/10 * * * * htmlship cd /srv/htmlship && .venv/bin/htmlship-purge --quiet
```

## Architecture

One FastAPI process hosts the landing page, JSON API, and view renderer. `HostRoutingMiddleware` classifies requests by host and prevents API routes from being served on the view host.

Postgres stores page metadata, owner-key/password hashes, expiry, view counts, and parent-version links. HTML bodies are stored as blobs, either in `LocalBlobStore` for development/tests or DigitalOcean Spaces in production.

## Project Layout

```text
src/htmlship/        Public Python library + CLI
src/htmlship_server/ FastAPI app, API routers, storage, database models
src/htmlship_mcp/    MCP server (stdio) — Python
npm/                 Node CLI + MCP server (publishes as the `htmlship` npm package)
web/                 Static landing page
tests/               API, client, CLI, MCP, landing, and view tests (Python)
alembic/             Database migrations
deploy/              Production configs (nginx, systemd)
scripts/             Deploy and smoke-test scripts
```

The npm package mirrors the Python CLI surface and reads/writes the same `~/.htmlship/keys.json` file format, so users can mix and match. The Node MCP server lives at `htmlship mcp` (subcommand) rather than a separate `htmlship-mcp` bin.

## License

Source code in this repository is licensed under the MIT License — see [`LICENSE`](./LICENSE).

The "HTMLShip" name and the brand assets in `web/assets/` (logo, favicons) are not covered by MIT and are reserved — see [`web/assets/LICENSE`](./web/assets/LICENSE). If you fork to run your own instance, please replace those files with your own branding.
