···11+# HTML Path Rewriting Example
22+33+This document demonstrates how HTML path rewriting works when serving sites via the `/s/:identifier/:site/*` route.
44+55+## Problem
66+77+When you create a static site with absolute paths like `/style.css` or `/images/logo.png`, these paths work fine when served from the root domain. However, when served from a subdirectory like `/s/alice.bsky.social/mysite/`, these absolute paths break because they resolve to the server root instead of the site root.
88+99+## Solution
1010+1111+The hosting service automatically rewrites absolute paths in HTML files to work correctly in the subdirectory context.
1212+1313+## Example
1414+1515+**Original HTML file (index.html):**
1616+```html
1717+<!DOCTYPE html>
1818+<html>
1919+<head>
2020+ <meta charset="UTF-8">
2121+ <title>My Site</title>
2222+ <link rel="stylesheet" href="/style.css">
2323+ <link rel="icon" href="/favicon.ico">
2424+ <script src="/app.js"></script>
2525+</head>
2626+<body>
2727+ <header>
2828+ <img src="/images/logo.png" alt="Logo">
2929+ <nav>
3030+ <a href="/">Home</a>
3131+ <a href="/about">About</a>
3232+ <a href="/contact">Contact</a>
3333+ </nav>
3434+ </header>
3535+3636+ <main>
3737+ <h1>Welcome</h1>
3838+ <img src="/images/hero.jpg"
3939+ srcset="/images/hero.jpg 1x, /images/hero@2x.jpg 2x"
4040+ alt="Hero">
4141+4242+ <form action="/submit" method="post">
4343+ <input type="text" name="email">
4444+ <button>Submit</button>
4545+ </form>
4646+ </main>
4747+4848+ <footer>
4949+ <a href="https://example.com">External Link</a>
5050+ <a href="#top">Back to Top</a>
5151+ </footer>
5252+</body>
5353+</html>
5454+```
5555+5656+**When accessed via `/s/alice.bsky.social/mysite/`, the HTML is rewritten to:**
5757+```html
5858+<!DOCTYPE html>
5959+<html>
6060+<head>
6161+ <meta charset="UTF-8">
6262+ <title>My Site</title>
6363+ <link rel="stylesheet" href="/s/alice.bsky.social/mysite/style.css">
6464+ <link rel="icon" href="/s/alice.bsky.social/mysite/favicon.ico">
6565+ <script src="/s/alice.bsky.social/mysite/app.js"></script>
6666+</head>
6767+<body>
6868+ <header>
6969+ <img src="/s/alice.bsky.social/mysite/images/logo.png" alt="Logo">
7070+ <nav>
7171+ <a href="/s/alice.bsky.social/mysite/">Home</a>
7272+ <a href="/s/alice.bsky.social/mysite/about">About</a>
7373+ <a href="/s/alice.bsky.social/mysite/contact">Contact</a>
7474+ </nav>
7575+ </header>
7676+7777+ <main>
7878+ <h1>Welcome</h1>
7979+ <img src="/s/alice.bsky.social/mysite/images/hero.jpg"
8080+ srcset="/s/alice.bsky.social/mysite/images/hero.jpg 1x, /s/alice.bsky.social/mysite/images/hero@2x.jpg 2x"
8181+ alt="Hero">
8282+8383+ <form action="/s/alice.bsky.social/mysite/submit" method="post">
8484+ <input type="text" name="email">
8585+ <button>Submit</button>
8686+ </form>
8787+ </main>
8888+8989+ <footer>
9090+ <a href="https://example.com">External Link</a>
9191+ <a href="#top">Back to Top</a>
9292+ </footer>
9393+</body>
9494+</html>
9595+```
9696+9797+## What's Preserved
9898+9999+Notice that:
100100+- ✅ Absolute paths are rewritten: `/style.css` → `/s/alice.bsky.social/mysite/style.css`
101101+- ✅ External URLs are preserved: `https://example.com` stays the same
102102+- ✅ Anchors are preserved: `#top` stays the same
103103+- ✅ The rewriting is safe and won't break your site
104104+105105+## Supported Attributes
106106+107107+The rewriter handles these HTML attributes:
108108+- `src` - images, scripts, iframes, videos, audio
109109+- `href` - links, stylesheets
110110+- `action` - forms
111111+- `data` - objects
112112+- `poster` - video posters
113113+- `srcset` - responsive images
114114+115115+## Testing Your Site
116116+117117+To test if your site works with path rewriting:
118118+119119+1. Upload your site to your PDS as a `place.wisp.fs` record
120120+2. Access it via: `https://hosting.wisp.place/s/YOUR_HANDLE/SITE_NAME/`
121121+3. Check that all resources load correctly
122122+123123+If you're using relative paths already (like `./style.css` or `../images/logo.png`), they'll work without any rewriting.
+130
hosting-service/README.md
···11+# Wisp Hosting Service
22+33+Minimal microservice for hosting static sites from the AT Protocol. Built with Hono and Bun.
44+55+## Features
66+77+- **Custom Domain Hosting**: Serve verified custom domains
88+- **Wisp.place Subdomains**: Serve registered `*.wisp.place` subdomains
99+- **DNS Hash Routing**: Support DNS verification via `hash.dns.wisp.place`
1010+- **Direct File Serving**: Access sites via `/s/:identifier/:site/*` (no DB lookup)
1111+- **Firehose Worker**: Listens to AT Protocol firehose for new `place.wisp.fs` records
1212+- **Automatic Caching**: Downloads and caches sites locally on first access or firehose event
1313+- **SSRF Protection**: Hardened fetch with timeout, size limits, and private IP blocking
1414+1515+## Routes
1616+1717+1. **Custom Domains** (`/*`)
1818+ - Serves verified custom domains (example.com)
1919+ - DB lookup: `custom_domains` table
2020+2121+2. **Wisp Subdomains** (`/*.wisp.place/*`)
2222+ - Serves registered subdomains (alice.wisp.place)
2323+ - DB lookup: `domains` table
2424+2525+3. **DNS Hash Routing** (`/hash.dns.wisp.place/*`)
2626+ - DNS verification routing for custom domains
2727+ - DB lookup: `custom_domains` by hash
2828+2929+4. **Direct Serving** (`/s.wisp.place/:identifier/:site/*`)
3030+ - Direct access without DB lookup
3131+ - `:identifier` can be DID or handle
3232+ - Fetches from PDS if not cached
3333+ - **Automatic HTML path rewriting**: Absolute paths (`/style.css`) are rewritten to relative paths (`/s/:identifier/:site/style.css`)
3434+3535+## Setup
3636+3737+```bash
3838+# Install dependencies
3939+bun install
4040+4141+# Copy environment file
4242+cp .env.example .env
4343+4444+# Run in development
4545+bun run dev
4646+4747+# Run in production
4848+bun run start
4949+```
5050+5151+## Environment Variables
5252+5353+- `DATABASE_URL` - PostgreSQL connection string
5454+- `PORT` - HTTP server port (default: 3001)
5555+- `BASE_HOST` - Base domain (default: wisp.place)
5656+5757+## Architecture
5858+5959+- **Hono**: Minimal web framework
6060+- **Postgres**: Database for domain/site lookups
6161+- **AT Protocol**: Decentralized storage
6262+- **Jetstream**: Firehose consumer for real-time updates
6363+- **Bun**: Runtime and file serving
6464+6565+## Cache Structure
6666+6767+```
6868+cache/sites/
6969+ did:plc:abc123/
7070+ sitename/
7171+ index.html
7272+ style.css
7373+ assets/
7474+ logo.png
7575+```
7676+7777+## Health Check
7878+7979+```bash
8080+curl http://localhost:3001/health
8181+```
8282+8383+Returns firehose connection status and last event time.
8484+8585+## HTML Path Rewriting
8686+8787+When serving sites via the `/s/:identifier/:site/*` route, HTML files are automatically processed to rewrite absolute paths to work correctly in the subdirectory context.
8888+8989+**What gets rewritten:**
9090+- `src` attributes (images, scripts, iframes)
9191+- `href` attributes (links, stylesheets)
9292+- `action` attributes (forms)
9393+- `poster`, `data` attributes (media)
9494+- `srcset` attributes (responsive images)
9595+9696+**What's preserved:**
9797+- External URLs (`https://example.com/style.css`)
9898+- Protocol-relative URLs (`//cdn.example.com/script.js`)
9999+- Data URIs (`data:image/png;base64,...`)
100100+- Anchors (`/#section`)
101101+- Already relative paths (`./style.css`, `../images/logo.png`)
102102+103103+**Example:**
104104+```html
105105+<!-- Original HTML -->
106106+<link rel="stylesheet" href="/style.css">
107107+<img src="/images/logo.png">
108108+109109+<!-- Served at /s/did:plc:abc123/mysite/ becomes -->
110110+<link rel="stylesheet" href="/s/did:plc:abc123/mysite/style.css">
111111+<img src="/s/did:plc:abc123/mysite/images/logo.png">
112112+```
113113+114114+This ensures sites work correctly when served from subdirectories without requiring manual path adjustments.
115115+116116+## Security
117117+118118+### SSRF Protection
119119+120120+All external HTTP requests are protected against Server-Side Request Forgery (SSRF) attacks:
121121+122122+- **5-second timeout** on all requests
123123+- **Size limits**: 1MB for JSON, 10MB default, 100MB for file blobs
124124+- **Blocked private IP ranges**:
125125+ - Loopback (127.0.0.0/8, ::1)
126126+ - Private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
127127+ - Link-local (169.254.0.0/16, fe80::/10)
128128+ - Cloud metadata endpoints (169.254.169.254)
129129+- **Protocol validation**: Only HTTP/HTTPS allowed
130130+- **Streaming with size enforcement**: Prevents memory exhaustion from large responses
···2233import type { app } from '@server'
4455-export const api = treaty<typeof app>('localhost:3000')
55+// Use the current host instead of hardcoded localhost
66+const apiHost = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8000'
77+88+export const api = treaty<typeof app>(apiHost)