{"id":"d326bdaa-0fd0-4aa2-a12f-3bf6dc9c8549","entityType":"agent","slug":"clawhub-dannylai999-clawsite-ai","name":"Clawsite","canonicalUrl":"https://www.xpersona.co/agent/clawhub-dannylai999-clawsite-ai","canonicalPath":"/agent/clawhub-dannylai999-clawsite-ai","generatedAt":"2026-10-11T10:52:27.846Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T07:12:28.743Z","emptyReason":null},"description":"Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call, atomic ful...","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s173ebs4e47b8b8m1a4v6qxfv1863waz:clawsite-ai","sourceUrl":"https://clawhub.ai/dannylai999/clawsite-ai","homepage":"https://clawhub.ai/dannylai999/skills/clawsite-ai","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/dannylai999/clawsite-ai","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/dannylai999/skills/clawsite-ai","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":61,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Clawsite technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T07:12:28.743Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T07:12:28.743Z","emptyReason":null},"stars":null,"forks":null,"downloads":1127,"packageName":null,"latestVersion":"1.0.5","tractionLabel":"1.1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T07:12:28.729Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T07:12:28.743Z","lastCrawledAt":"2026-10-11T07:12:28.729Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T07:12:28.729Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.5","createdAt":"2026-05-11T03:11:47.918Z","changelog":"Version 1.0.5 – Documentation Update - Expanded SKILL.md with detailed instructions on how to handle the `409 quota_exceeded` error for `/deploy` requests. - Clarified that the error indicates a deploy rate limit (30 per hour) and that users should not attempt to re-register. - No functional or API changes; update is documentation-only for improved user guidance.","fileCount":7,"zipByteSize":13406},{"version":"1.0.4","createdAt":"2026-05-11T02:35:58.689Z","changelog":"- Increased deploy frequency limit from 10/hour to 30/hour per account - Increased purge cache frequency limit from 5/hour to 15/hour per account - Updated version to 1.0.4 in SKILL.md","fileCount":6,"zipByteSize":11141},{"version":"1.0.3","createdAt":"2026-05-07T02:17:58.301Z","changelog":"- Updated required environment variable descriptions for clarity in documentation. - Added a new allowed error code internal_error to the Errors section in SKILL.md. - Improved formatting and details in environment variable and API documentation. - Added vitest.e2e.config.ts file for end-to-end testing configuration.","fileCount":6,"zipByteSize":11138},{"version":"1.0.2","createdAt":"2026-05-04T12:17:25.085Z","changelog":"- Changelog for version 1.0.2: - Updated documentation for the registration endpoint's idempotency: clarified that `/v1/register` with an existing MBID identity always returns the same `apiKey` and `sites`, and there is no API key rotation. - Minor improvements to the description and formatting for clarity. - No API or behavioral changes; this is a documentation update only.","fileCount":5,"zipByteSize":9819},{"version":"1.0.1","createdAt":"2026-05-04T09:59:01.627Z","changelog":"- Improved `/v1/register` endpoint idempotency: repeated calls with the same email now return the same `apiKey` and `sites`, making re-provisioning reliable. - Clarified SKILL.md documentation for the registration and recovery flows. - No functional API changes aside from the stable key re-provisioning guarantee.","fileCount":5,"zipByteSize":9822},{"version":"1.0.0","createdAt":"2026-05-04T09:25:55.391Z","changelog":"Initial release of clawsite-ai — static web hosting for AI agents. - Provides instant, HTTPS-enabled subdomains for AI-generated static sites (HTML/CSS/JS/images) via atomic zip uploads. - Features seamless API for full-site deploys, CDN cache auto-invalidation, and easy quota inspection. - Includes public site URLs, pretty routing, and free-tier signup with no credit card. - Site management supports deploy, purge cache, delete site, and standalone registration using email OTP. - Enforces generous, well-documented quotas and strict extension/file size limits for reliability.","fileCount":5,"zipByteSize":9798}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s173ebs4e47b8b8m1a4v6qxfv1863waz:clawsite-ai","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s173ebs4e47b8b8m1a4v6qxfv1863waz:clawsite-ai` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/dannylai999/clawsite-ai before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T10:52:27.842Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-dannylai999-clawsite-ai/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T07:12:28.743Z","emptyReason":null},"readme":"Skill: Clawsite\n\nOwner: dannylai999\n\nSummary: Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call, atomic ful...\n\nTags: latest:1.0.5\n\nVersion history:\n\nv1.0.5 | 2026-05-11T03:11:47.918Z | auto\n\nVersion 1.0.5 – Documentation Update\n\n- Expanded SKILL.md with detailed instructions on how to handle the `409 quota_exceeded` error for `/deploy` requests.\n- Clarified that the error indicates a deploy rate limit (30 per hour) and that users should not attempt to re-register.\n- No functional or API changes; update is documentation-only for improved user guidance.\n\nv1.0.4 | 2026-05-11T02:35:58.689Z | auto\n\n- Increased deploy frequency limit from 10/hour to 30/hour per account\n- Increased purge cache frequency limit from 5/hour to 15/hour per account\n- Updated version to 1.0.4 in SKILL.md\n\nv1.0.3 | 2026-05-07T02:17:58.301Z | auto\n\n- Updated required environment variable descriptions for clarity in documentation.\n- Added a new allowed error code internal_error to the Errors section in SKILL.md.\n- Improved formatting and details in environment variable and API documentation.\n- Added vitest.e2e.config.ts file for end-to-end testing configuration.\n\nv1.0.2 | 2026-05-04T12:17:25.085Z | auto\n\n- Changelog for version 1.0.2:\n- Updated documentation for the registration endpoint's idempotency: clarified that `/v1/register` with an existing MBID identity always returns the same `apiKey` and `sites`, and there is no API key rotation.\n- Minor improvements to the description and formatting for clarity.\n- No API or behavioral changes; this is a documentation update only.\n\nv1.0.1 | 2026-05-04T09:59:01.627Z | auto\n\n- Improved `/v1/register` endpoint idempotency: repeated calls with the same email now return the same `apiKey` and `sites`, making re-provisioning reliable.\n- Clarified SKILL.md documentation for the registration and recovery flows.\n- No functional API changes aside from the stable key re-provisioning guarantee.\n\nv1.0.0 | 2026-05-04T09:25:55.391Z | auto\n\nInitial release of clawsite-ai — static web hosting for AI agents.\n\n- Provides instant, HTTPS-enabled subdomains for AI-generated static sites (HTML/CSS/JS/images) via atomic zip uploads.\n- Features seamless API for full-site deploys, CDN cache auto-invalidation, and easy quota inspection.\n- Includes public site URLs, pretty routing, and free-tier signup with no credit card.\n- Site management supports deploy, purge cache, delete site, and standalone registration using email OTP.\n- Enforces generous, well-documented quotas and strict extension/file size limits for reliability.\n\nArchive index:\n\nArchive v1.0.5: 7 files, 13406 bytes\n\nFiles: CLAUDE.md (11336b), package.json (844b), README.md (1202b), skill-card.md (2584b), SKILL.md (10128b), vitest.e2e.config.ts (1566b), _meta.json (130b)\n\nFile v1.0.5:SKILL.md\n\n---\nname: clawsite-ai\ndescription: >-\n  Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai\n  URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call,\n  atomic full-site replace, automatic CDN cache invalidation. Use when your\n  agent generates static content (portfolios, news pages, link hubs,\n  single-purpose landing pages) and wants to share it as a public URL.\n  Free tier included, no credit card needed.\nversion: 1.0.5\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - CLAWSITE_API_KEY\n        - CLAWSITE_SITE_ID\n    primaryEnv: CLAWSITE_API_KEY\n    homepage: https://clawsite.ai\n---\n\n# Clawsite.ai — Static Website Hosting for AI Agents\n\n## When to Use Clawsite\n\n- Your agent generates static HTML / CSS / JS / images and wants to publish them as a public URL\n- You want a zero-config hosting account with HTTPS, CloudFront CDN, and atomic deploys\n- You want a memorable random URL like `happy-otter-42.clawsite.ai` (the slug is auto-generated; you can't pick it)\n\n## Quick Start\n\n**Your sandbox already has an account provisioned.** Check these env vars before doing anything else:\n\n| Env var | Required? | Purpose |\n|---|---|---|\n| `CLAWSITE_API_KEY` | **required** | Bearer token for all authenticated endpoints (`csk_live_*` format) |\n| `CLAWSITE_SITE_ID` | **required** | Your assigned site identifier (`site_<ulid>` format) |\n| `CLAWSITE_URL` | informational | Your live site URL, the one to share with the user (e.g. `https://happy-otter-42.clawsite.ai`). If unset, derive from `GET /v1/sites`. |\n| `CLAWSITE_API_URL` | optional | API base URL. **Defaults to `https://api.clawsite.ai` if unset.** Dev sandboxes override to `https://api.dev.clawsite.ai`. |\n\nIf `CLAWSITE_API_KEY` or `CLAWSITE_SITE_ID` is unset, see \"Standalone Registration\" at the bottom.\n\n> Examples below use `$CLAWSITE_API_URL` literally; if it's unset, fall back to `https://api.clawsite.ai`.\n\n**API base: `$CLAWSITE_API_URL`/v1**\n\nAll authenticated endpoints require `Authorization: Bearer $CLAWSITE_API_KEY`.\n\n### 1. Deploy a directory of static files\n\nThe deploy endpoint takes a `.zip` of your site contents (max 4 MB compressed; expanded contents must fit the per-site quota — see \"Quotas\" below).\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\nAuthorization: Bearer $CLAWSITE_API_KEY\nContent-Type: application/zip\n\n(body: raw bytes of the .zip)\n\nWorkflow:\n\n1. Create your files in a directory:\n   ```\n   site/\n     index.html\n     style.css\n     app.js\n     images/logo.png\n   ```\n2. Zip the **contents** of the directory (no parent directory inside the zip):\n   ```\n   cd site/ && zip -r ../site.zip .\n   ```\n3. POST the zip:\n   ```\n   curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\"\n   ```\n\n-> Returns: `siteId`, `url`, `fileCount`, `sizeBytes`, `deployedAt` (Unix seconds)\n\n```json\n{\n  \"siteId\": \"site_...\",\n  \"url\": \"https://happy-otter-42.clawsite.ai\",\n  \"fileCount\": 12,\n  \"sizeBytes\": 458231,\n  \"deployedAt\": 1744732800\n}\n```\n\n**Atomic full-site replacement:** any files from the previous deploy that are NOT in the new zip get deleted. Deploy = full snapshot, not incremental upload.\n\n**Cache:** every deploy automatically invalidates CloudFront cache. Manual purge below is rarely needed.\n\n**Routing:** the path inside the zip becomes the URL path. `index.html` at the zip root is served at `/`. Subdirectories work: `images/logo.png` is at `/images/logo.png`. For pretty URLs without `.html`, name files like `about/index.html` and link as `/about/`.\n\n### 2. Show the user their site\n\nThe site is live at `$CLAWSITE_URL` immediately after a successful deploy. Tell the user:\n\n> \"Your site is live at $CLAWSITE_URL\"\n\n### 3. Purge CloudFront cache (rarely needed)\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/purge-cache\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n(no body)\n\n-> Returns: `siteId`, `purgedAt`\n\n```json\n{ \"siteId\": \"site_...\", \"purgedAt\": 1744732800 }\n```\n\nUse this only if the cache is serving stale content unrelated to a deploy. Normal deploys auto-invalidate.\n\n### 4. List sites and check quota usage\n\nGET $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n-> Returns: array of `{ siteId, slug, url, sizeBytes, fileCount, lastDeployAt }`\n\n```json\n{\n  \"sites\": [{\n    \"siteId\": \"site_...\",\n    \"slug\": \"happy-otter-42\",\n    \"url\": \"https://happy-otter-42.clawsite.ai\",\n    \"sizeBytes\": 458231,\n    \"fileCount\": 12,\n    \"lastDeployAt\": 1744732800\n  }]\n}\n```\n\nUse this to verify your `CLAWSITE_SITE_ID` matches and to inspect current usage vs quotas.\n\n## Other Endpoints\n\n### Delete a site\n\nDELETE $CLAWSITE_API_URL/v1/sites/{siteId}\nAuthorization: Bearer $CLAWSITE_API_KEY\n\nPermanently deletes the site and every file under it. The slug is tombstoned (kept reserved forever) so the URL can never be re-used by another account — this prevents URL takeover of a previously-shared link.\n\n## Standalone Registration (no sandbox env vars)\n\nIf you're running outside a ZenClaw sandbox and `CLAWSITE_API_KEY` isn't pre-set, register via email OTP. Verification is delegated to MBID (MixerBox ID).\n\n**Step 1** — request a 6-digit code via email:\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"email\": \"your-email@example.com\" }\n```\n\n-> Returns: `{ \"challengeId\": \"<JWT>\" }`\n\n**Step 2** — verify (after the 6-digit code arrives in your inbox):\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"challengeId\": \"<JWT from step 1>\", \"code\": \"123456\" }\n```\n\n-> Returns: `accountId`, `apiKey`, and a default `sites[]` entry. **Save the `apiKey` immediately — it cannot be recovered.**\n\nYou can also create additional sites later via:\n\nPOST $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $apiKey\n\n(no body needed; slug is auto-assigned)\n\n> Note: in v1 the per-account site quota is 1, so this returns 409 `quota_exceeded` if you already have one site.\n\n## Quotas (v1)\n\n| Item | Limit |\n|---|---|\n| Sites per account | **1** |\n| Storage per site | **3 MB** (uncompressed total) |\n| Max single file | **1 MB** |\n| Max files per site | **100** |\n| Max compressed zip body | **4 MB** |\n| Deploy frequency | **30 / hour** per account |\n| Purge frequency | **15 / hour** per account |\n| Bandwidth | **unlimited** |\n\n**Allowed file extensions:** `html`, `css`, `js`, `json`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico`, `woff2`, `txt`, `md`.\n\nAnything else (e.g. `.php`, `.exe`, `.py`) → 400 `unsupported_extension`.\n\n## Errors\n\nAll errors return JSON:\n\n```json\n{ \"error\": { \"code\": \"<machine-code>\", \"message\": \"<human-readable>\" } }\n```\n\n| Code | HTTP | Cause |\n|---|---|---|\n| `unauthorized` | 401 | Missing or invalid API key |\n| `missing_fields` | 400 | Required fields absent or malformed (e.g. invalid email shape on register) |\n| `not_found` | 404 | Site doesn't belong to your account, or doesn't exist |\n| `quota_exceeded` | 409 | Sites limit, storage limit, or rate limit hit |\n| `unsupported_extension` | 400 | File extension not in the whitelist above |\n| `file_too_large` | 400 | Single file > 1 MB, or zip body > 4 MB |\n| `invalid_zip` | 400 | Body not a valid zip, missing body, or contains paths with `..` / absolute paths |\n| `mbid_error` | upstream | Forwarded from MBID's email-verify endpoints (`mx_record_not_found`, `domain_typo`, `too_many_request`, `incorrect_verification_code`) — only relevant during email registration |\n| `internal_error` | 500 | Server-side inconsistent state. Should never happen — report if seen. |\n\n### 🛑 How to handle `409 quota_exceeded` on `/deploy` — DO NOT RE-REGISTER\n\nA `409 quota_exceeded` on `POST /v1/sites/{siteId}/deploy` means **you hit the per-hour deploy rate limit (30/hr per account)**. It is NOT an authentication or account problem. Your `CLAWSITE_API_KEY` and `CLAWSITE_SITE_ID` are perfectly valid.\n\nThe ONLY correct response is to **tell the user the rate limit was hit and they can try again in less than 1 hour**. For example:\n\n> \"I've hit ClawSite's deploy rate limit (30 deploys/hour per account). Your site is still live at $CLAWSITE_URL — try the next deploy in a few minutes / next hour, whichever comes first.\"\n\n**Forbidden actions on 409 (these will destroy the user's existing site):**\n\n- ❌ Do NOT `DELETE` the existing site to \"free up\" or \"reset\" quota — DELETE permanently tombstones the slug (the URL is lost forever) and does **not** clear the quota counter.\n- ❌ Do NOT call `/v1/register` again hoping for a fresh `apiKey` — `/v1/register` is idempotent on MBID identity; it returns the SAME account and SAME `apiKey` regardless of how many times you call it. Calling it does not bypass quota.\n- ❌ Do NOT use the \"Standalone Registration\" section below — that is ONLY for users running outside a pre-provisioned sandbox (i.e. when `CLAWSITE_API_KEY` is genuinely unset). If you have a `CLAWSITE_API_KEY` env var, you are already registered; do not register again.\n- ❌ Do NOT try to \"DELETE + re-register fresh\" to dodge the quota — see the bold warning under the Idempotency note below.\n\n**Same rules apply to `429 / 503 / 500` from `/deploy`**: report to the user, do not destructive-reset the account.\n\n**Idempotency note:** `/v1/register` is idempotent on MBID identity. Re-calling it with the same MBID-verified email (or partner-mode `mbidUserId`) returns the existing `accountId`, the same `apiKey` that was issued on first register, and the existing `sites`. There is no rotation API.\n\n**⚠️ DANGER — only delete a site if the user *explicitly* asked to delete it:** if you genuinely need a fresh slug (rare — the user said \"give me a new URL\"), `DELETE /v1/sites/{siteId}` then re-register. This is **destructive and irreversible**: the deleted slug is permanently tombstoned (anti URL-takeover) and can never be re-used. The user loses any URL they previously shared. **Never** do this as a side-effect of error handling — only when the user has explicitly told you to delete the existing site.\n\nFile v1.0.5:README.md\n\n# ClawSite\n\nAgent-first static website hosting microservice. Lets an AI agent claim hosting space and ship a site with zero human interaction.\n\n- **Production domain:** `clawsite.ai` — API at `api.clawsite.ai`\n- **Development domain:** `dev.clawsite.ai` — API at `api.dev.clawsite.ai`\n- **First partner:** ZenClaw (`MixerBox/zenclaw`)\n- **Skill:** `clawsite-ai` (published to ClawHub)\n- **Sibling service:** [`MixerBox/clawmail`](https://github.com/MixerBox/clawmail) — same operational shape (Terraform, esbuild, single-Lambda + CloudFront-fronted S3, ClawHub-published skill)\n\nSee `CLAUDE.md` for the architecture / deploy reference, `docs/superpowers/specs/` for the design spec, `tests/e2e/README.md` for live-API smoke tests.\n\n## Status\n\n✅ Deployed live to dev + prod. Both AWS account `974718210214` (us-east-1).\n\n| Env | API | Site URL pattern |\n|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` |\n\nZenClaw partner-mode integration is the active code path. Email-OTP registration is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps.\n\nFile v1.0.5:_meta.json\n\n{\n  \"ownerId\": \"kn7e574f4e1ewbqp1xvm49jqss862wx3\",\n  \"slug\": \"clawsite-ai\",\n  \"version\": \"1.0.5\",\n  \"publishedAt\": 1778469107918\n}\n\nFile v1.0.5:CLAUDE.md\n\n# ClawSite\n\nAgent-first static website hosting microservice, modeled after [ClawMail](https://github.com/MixerBox/clawmail).\n\n## Status (2026-04-29)\n\n✅ **v1 deployed to dev + prod, smoke-tested live, partner mode confirmed working end-to-end.**\n\n| Env | API | Site URL pattern | Partner secret on |\n|---|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` | `claws-instance-controller-staging` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` | `claws-instance-controller` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n\nCloudFront distribution IDs: dev `E3VZOWBAWTVYBK`, prod `ESXYWGNPX39MP`.\n\nBoth envs live in AWS account `974718210214` (us-east-1), same account as ClawMail. Route53 zone `clawsite.ai` is shared between dev/prod.\n\n**Email-OTP path is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps in MBID `TABLE_APP`.** Once that's done, set `MBID_SERVER_KEY` on the api Lambda env (Terraform var: `mbid_server_key`) and email registration unlocks. Partner mode (the only path ZenClaw uses today) doesn't depend on this.\n\n## Reference repos\n\n- `~/Work/MixerBox/clawmail/` — the structural model. When in doubt about a pattern (`globalThis.__routes`, Vitest mocks, Terraform), look at how ClawMail does it.\n- `~/Work/MixerBox/zenclaw/` — the first partner. Plan 2 (`configure-clawsite` controller action + sandbox env injection + `clawsite-ai` skill install) lands there.\n- `~/Work/MixerBox/microservice/microservice/kubernetes/livapp/apps/mb-id/` — MBID service we delegate email verification to.\n\n## Quick reference\n\n- **Language:** TypeScript (Node.js 20, ES2022 modules)\n- **Test:** `npx vitest run`\n- **Typecheck:** `npx tsc --noEmit`\n- **Build:** `node build.mjs` (outputs `.mjs` to `dist/`)\n- **Deploy** (Terraform: `terraform/` dir, two workspaces `dev` + `prod`):\n  ```bash\n  # build + zip Lambda artifact (load-bearing filename: api.zip)\n  node build.mjs\n  cd dist && zip -j api.zip api.mjs api.mjs.map\n\n  # dev — uses claws-instance-controller-staging partner secret\n  cd ../terraform && terraform workspace select dev && terraform apply \\\n    -var \"site_domain=dev.clawsite.ai\" \\\n    -var \"api_domain=api.dev.clawsite.ai\" \\\n    -var \"environment=staging\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller-staging \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n\n  # prod — uses claws-instance-controller partner secret\n  terraform workspace select prod && terraform apply \\\n    -var \"site_domain=clawsite.ai\" \\\n    -var \"api_domain=api.clawsite.ai\" \\\n    -var \"environment=prod\" \\\n    -var \"mbid_api_base_url=https://api.id.mixerbox.com\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n  ```\n\n  The `partner_secret` flow: ZenClaw's controller Lambda holds `PARTNER_CLAWSITE_SECRET` as an env var (set once via `aws lambda update-function-configuration`); ClawSite's Terraform reads it at apply time so the two sides cannot drift. Same pattern as ClawMail's `PARTNER_CLAWMAIL_SECRET`.\n- **Hot-patch Lambda code only** (when only src/ changed, no infra): `node build.mjs && cd dist && zip -j api.zip api.mjs api.mjs.map && for fn in clawsite-staging-api clawsite-api; do aws lambda update-function-code --function-name \"$fn\" --zip-file fileb://api.zip; done`\n- **Publish skill to ClawHub:** `clawhub publish .` (run from repo root; `SKILL.md` is the manifest)\n- **Spec:** [`docs/superpowers/specs/2026-04-15-clawsite-design.md`](docs/superpowers/specs/2026-04-15-clawsite-design.md)\n\n### Resource naming convention\n\n`local.prefix` in `terraform/main.tf` derives the AWS resource name prefix from `var.environment`:\n\n- `var.environment = \"prod\"` → `local.prefix = \"clawsite\"` → `clawsite-api`, `clawsite-manifest`, `clawsite-sites-<account>`\n- anything else (typically `staging` for dev workspace) → `local.prefix = \"clawsite-${env}\"` → `clawsite-staging-api`, `clawsite-staging-manifest`, `clawsite-staging-sites-<account>`\n\nThe dev workspace uses `environment=staging` to match ZenClaw's `claws-instance-controller-staging` controller naming. **`-var environment=...` is required on every apply** (no default — pass `prod` for prod, `staging` for dev).\n\n## Architecture (single Lambda)\n\n| Lambda | Trigger | Purpose |\n|---|---|---|\n| `api` | API Gateway HTTP API (`ANY /{proxy+}`) | All REST endpoints — register, sites CRUD, deploy, purge-cache |\n\nThere used to be a `cleanup` Lambda intended for daily housekeeping, but it had no EventBridge trigger and nothing to clean (quota counters self-expire via DDB TTL; slug tombstones are by-design permanent). Removed 2026-04-30. If real housekeeping logic emerges later, re-add the Lambda + EventBridge rule together.\n\nStorage:\n- **DynamoDB** single table `clawsite[-staging]-manifest` — accounts, API keys, sites, slug reservations, quota counters. GSIs: `byApiKeyHash`, `byMbid`.\n- **S3** `clawsite[-staging]-sites-<account-id>` — static site files, partitioned by slug prefix.\n- **CloudFront** distribution + CloudFront Function for `<slug>` host → S3 prefix routing.\n\n## MBID integration (email OTP)\n\nEmail OTP registration delegates to **MBID** (`api.id.{dev.,}mixerbox.com`). ClawSite calls:\n- `POST /api/request_email_verify` → returns a `verifyToken` JWT we pass back as our `challengeId`\n- `PUT /api/verify_email` → returns `user.uuid` which becomes our `mbidUserId`\n\nNet effect: **every ClawSite account is keyed by an MBID UUID**. Partner-mode registration (ZenClaw) and email-OTP registration both deduplicate via the same `byMbid` GSI — one user, one ClawSite account, regardless of how they arrived.\n\nTo activate email-OTP path:\n1. Register `clawsite-dev` and `clawsite-prod` as apps in MBID's `TABLE_APP` to obtain `serverKey` values\n2. Update Lambda env vars: `MBID_SERVER_KEY` (the serverKey for that env) and `MBID_API_BASE_URL` (already set per env)\n\nEmail-OTP code is in place with mocked tests; real serverKey just unlocks the production path.\n\n### Removed from original spec (no longer relevant)\n\nThe spec originally called for self-hosted SES + SQS + a `deliver-email` Lambda. The MBID rework dropped all of that:\n\n- ❌ No SES domain identity / DKIM / SPF / DMARC\n- ❌ No SQS verification queue / DLQ\n- ❌ No `deliver-email` Lambda (architecture went 3 → 2 Lambdas)\n- ❌ No DynamoDB `challenge#*` items (MBID's own `verifyToken` JWT is stateless on our side)\n\nIf re-reading the spec, treat any reference to those items as historical.\n\n## Branching\n\n- `main` — what's deployed to prod (auto-deploy is manual via `terraform apply` on prod workspace)\n- `develop` — staging-equivalent; merges back to `main` after smoke tests\n- Bug fixes / features land via `fix/*` or `feat/*` branches → develop → main\n\n## Project structure\n\n```\nsrc/\n├── handlers/\n│   ├── api.ts              # API Gateway entry; regex route table on globalThis.__routes\n│   └── routes/             # one file per endpoint group, registered via side-effect imports\n│       ├── register.ts\n│       ├── sites.ts\n│       ├── deploy.ts\n│       └── purge.ts\n├── lib/                    # AWS-thin utilities\n│   ├── auth.ts             # SHA-256 + timing-safe apiKey verify + partner secret check\n│   ├── cloudfront.ts       # invalidatePrefix() helper\n│   ├── dynamo.ts           # DDB DocumentClient + PK/SK builders + table/GSI names + hourWindow()\n│   ├── errors.ts           # AppError class, JSON response helpers, CORS headers\n│   ├── id.ts               # acc_, site_, csk_live_* ID generators\n│   ├── mbid.ts             # MBID thin client (requestEmailVerify, verifyEmail)\n│   ├── quota.ts            # Hourly window counters with atomic increments\n│   ├── s3.ts               # S3 client + bucket name\n│   ├── slug.ts             # Random adjective-animal-NN slug + fallback hex slug\n│   ├── validate.ts         # File extension whitelist, size limits, QUOTA constants\n│   └── zip.ts              # Streaming zip extraction with validation\n└── middleware/\n    └── auth.ts             # Bearer apiKey → AuthContext via byApiKeyHash GSI\nbuild.mjs                   # esbuild bundler (entry: api.ts, cleanup.ts)\nterraform/                  # AWS infra; workspaces dev + prod\ntests/                      # Vitest tree mirrors src/\ndocs/superpowers/{specs,plans}/  # Design specs and execution plans\n```\n\n## Key patterns\n\n### Route registration\nRoutes register via side-effect imports in `src/handlers/api.ts`. Each route file calls `route(method, path, handler)` at module level. Routes are stored in `globalThis.__routes` to avoid ESM circular-import TDZ issues — same pattern as ClawMail.\n\nPublic (no-auth) routes: `route('POST', '/path', handler, { public: true })` — currently only `/v1/register`.\n\nThe `route()` function defensively initializes `globalThis.__routes` because esbuild bundling can place dependency module top-level code before the entry's initialization. See comment in `api.ts` for context.\n\n### Auth\n`Authorization: Bearer <token>`:\n- `csk_live_*` → SHA-256 hash → DynamoDB GSI lookup `byApiKeyHash` (per-account API key)\n- Partner secret → on `POST /v1/register` only, matched in handler before falling through to email-mode body shapes\n\n### Single-table DynamoDB\nActive PK/SK prefixes (live in code):\n- `account#<id>` + `meta` / `apikey#<hash>` / `site#<id>` / `quota#<action>#<window>`\n- `slug#<slug>` + `meta` (global slug reservation; status flips to `tombstoned` on site delete and stays forever — prevents URL takeover)\n\n`account#<id>` + `meta` includes a `sitesCount` numeric field, atomically updated by site CRUD operations to enforce the per-account site quota race-free (see `src/handlers/routes/sites.ts`).\n\nGSIs: `byApiKeyHash` (apiKey lookup → accountId), `byMbid` (MBID UUID → accountId for cross-flow idempotency).\n\n### Quota enforcement\n- Per-account site count: atomic via `sitesCount` counter on account meta + conditional update inside the site-creation TransactWrite. Can never race.\n- Per-hour deploy / purge frequency: hourly window counter in `src/lib/quota.ts`, gated on a single conditional `ADD count :one` inside the action handler.\n\n### ClawHub publishing\n`SKILL.md` at the repo root is the agent-facing API doc + ClawHub manifest. Published via `clawhub publish .`. Skill slug: `clawsite-ai`. Once Plan 2 lands, ZenClaw sandboxes will `clawhub install clawsite-ai` during provisioning.\n\n### Testing\nVitest with `vi.mock('@lib/dynamo')`. Route handler tests also mock `@handlers/api` to export a no-op `route` function, avoiding circular imports. Path aliases live in both `tsconfig.json` and `vitest.config.ts` — keep them in sync.\n\n## Known gaps / follow-ups\n\n- **Plan 2 (ZenClaw integration)** is in flight in `~/Work/MixerBox/zenclaw` on `feat/configure-clawsite-action`. Adds the `configure-clawsite` controller action + manifest fields. Sandbox-side env injection + `clawhub install clawsite-ai` are still TODO.\n\nFile v1.0.5:skill-card.md\n\n## Description:\n\nStatic website hosting for AI agents with HTTPS, zip-based static asset deploys, atomic full-site replacement, and automatic CDN cache invalidation.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[dannylai999](https://clawhub.ai/user/dannylai999)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and AI agents use this skill to publish generated static websites, portfolios, link hubs, and landing pages to a public ClawSite URL. It guides agents through authenticated deployment, quota checks, cache purging, and safe handling of destructive operations.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A misconfigured or attacker-controlled API endpoint could expose the ClawSite API key.\n\nMitigation: Use only documented ClawSite API hosts, confirm overrides before use, and avoid printing or embedding API keys in deployed static files.\n\nRisk: The skill can publish public content and includes a delete endpoint that permanently tombstones a site URL.\n\nMitigation: Require explicit user confirmation before publishing public content or deleting a site, and explain that deletion is irreversible.\n\nRisk: The artifact includes production Terraform and Lambda operation commands that could affect live infrastructure when cloud credentials are present.\n\nMitigation: Do not run backend infrastructure commands unless the user intends to operate the ClawSite service and the agent environment is authorized for that purpose.\n\nRisk: Deploy rate limits may return quota errors that could be mistaken for an account problem.\n\nMitigation: On deploy quota errors, tell the user to wait for the rate limit window instead of deleting the site or re-registering.\n\n## Reference(s):\n\n- [ClawSite homepage](https://clawsite.ai)\n- [ClawHub skill page](https://clawhub.ai/dannylai999/skills/clawsite-ai)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown with inline shell commands and API request examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Guidance may include public deployment URLs, quota status, and confirmation prompts for destructive actions.]\n\n## Skill Version(s):\n\n1.0.5 (source: frontmatter and server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v1.0.5:package.json\n\n{\n  \"name\": \"clawsite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Agent-first static website hosting microservice (clawsite.ai)\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"node build.mjs\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:e2e\": \"vitest run --config vitest.e2e.config.ts\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-cloudfront\": \"^3.1007.0\",\n    \"@aws-sdk/client-dynamodb\": \"^3.1007.0\",\n    \"@aws-sdk/client-s3\": \"^3.1007.0\",\n    \"@aws-sdk/lib-dynamodb\": \"^3.1007.0\",\n    \"ulid\": \"^3.0.2\",\n    \"yauzl\": \"^3.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/aws-lambda\": \"^8.10.161\",\n    \"@types/node\": \"^25.4.0\",\n    \"@types/yauzl\": \"^2.10.3\",\n    \"@types/yazl\": \"^3.3.1\",\n    \"esbuild\": \"^0.27.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"yazl\": \"^3.3.1\"\n  }\n}\n\nArchive v1.0.4: 6 files, 11141 bytes\n\nFiles: CLAUDE.md (11336b), package.json (844b), README.md (1202b), SKILL.md (8000b), vitest.e2e.config.ts (1566b), _meta.json (130b)\n\nFile v1.0.4:SKILL.md\n\n---\nname: clawsite-ai\ndescription: >-\n  Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai\n  URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call,\n  atomic full-site replace, automatic CDN cache invalidation. Use when your\n  agent generates static content (portfolios, news pages, link hubs,\n  single-purpose landing pages) and wants to share it as a public URL.\n  Free tier included, no credit card needed.\nversion: 1.0.4\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - CLAWSITE_API_KEY\n        - CLAWSITE_SITE_ID\n    primaryEnv: CLAWSITE_API_KEY\n    homepage: https://clawsite.ai\n---\n\n# Clawsite.ai — Static Website Hosting for AI Agents\n\n## When to Use Clawsite\n\n- Your agent generates static HTML / CSS / JS / images and wants to publish them as a public URL\n- You want a zero-config hosting account with HTTPS, CloudFront CDN, and atomic deploys\n- You want a memorable random URL like `happy-otter-42.clawsite.ai` (the slug is auto-generated; you can't pick it)\n\n## Quick Start\n\n**Your sandbox already has an account provisioned.** Check these env vars before doing anything else:\n\n| Env var | Required? | Purpose |\n|---|---|---|\n| `CLAWSITE_API_KEY` | **required** | Bearer token for all authenticated endpoints (`csk_live_*` format) |\n| `CLAWSITE_SITE_ID` | **required** | Your assigned site identifier (`site_<ulid>` format) |\n| `CLAWSITE_URL` | informational | Your live site URL, the one to share with the user (e.g. `https://happy-otter-42.clawsite.ai`). If unset, derive from `GET /v1/sites`. |\n| `CLAWSITE_API_URL` | optional | API base URL. **Defaults to `https://api.clawsite.ai` if unset.** Dev sandboxes override to `https://api.dev.clawsite.ai`. |\n\nIf `CLAWSITE_API_KEY` or `CLAWSITE_SITE_ID` is unset, see \"Standalone Registration\" at the bottom.\n\n> Examples below use `$CLAWSITE_API_URL` literally; if it's unset, fall back to `https://api.clawsite.ai`.\n\n**API base: `$CLAWSITE_API_URL`/v1**\n\nAll authenticated endpoints require `Authorization: Bearer $CLAWSITE_API_KEY`.\n\n### 1. Deploy a directory of static files\n\nThe deploy endpoint takes a `.zip` of your site contents (max 4 MB compressed; expanded contents must fit the per-site quota — see \"Quotas\" below).\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\nAuthorization: Bearer $CLAWSITE_API_KEY\nContent-Type: application/zip\n\n(body: raw bytes of the .zip)\n\nWorkflow:\n\n1. Create your files in a directory:\n   ```\n   site/\n     index.html\n     style.css\n     app.js\n     images/logo.png\n   ```\n2. Zip the **contents** of the directory (no parent directory inside the zip):\n   ```\n   cd site/ && zip -r ../site.zip .\n   ```\n3. POST the zip:\n   ```\n   curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\"\n   ```\n\n-> Returns: `siteId`, `url`, `fileCount`, `sizeBytes`, `deployedAt` (Unix seconds)\n\n```json\n{\n  \"siteId\": \"site_...\",\n  \"url\": \"https://happy-otter-42.clawsite.ai\",\n  \"fileCount\": 12,\n  \"sizeBytes\": 458231,\n  \"deployedAt\": 1744732800\n}\n```\n\n**Atomic full-site replacement:** any files from the previous deploy that are NOT in the new zip get deleted. Deploy = full snapshot, not incremental upload.\n\n**Cache:** every deploy automatically invalidates CloudFront cache. Manual purge below is rarely needed.\n\n**Routing:** the path inside the zip becomes the URL path. `index.html` at the zip root is served at `/`. Subdirectories work: `images/logo.png` is at `/images/logo.png`. For pretty URLs without `.html`, name files like `about/index.html` and link as `/about/`.\n\n### 2. Show the user their site\n\nThe site is live at `$CLAWSITE_URL` immediately after a successful deploy. Tell the user:\n\n> \"Your site is live at $CLAWSITE_URL\"\n\n### 3. Purge CloudFront cache (rarely needed)\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/purge-cache\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n(no body)\n\n-> Returns: `siteId`, `purgedAt`\n\n```json\n{ \"siteId\": \"site_...\", \"purgedAt\": 1744732800 }\n```\n\nUse this only if the cache is serving stale content unrelated to a deploy. Normal deploys auto-invalidate.\n\n### 4. List sites and check quota usage\n\nGET $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n-> Returns: array of `{ siteId, slug, url, sizeBytes, fileCount, lastDeployAt }`\n\n```json\n{\n  \"sites\": [{\n    \"siteId\": \"site_...\",\n    \"slug\": \"happy-otter-42\",\n    \"url\": \"https://happy-otter-42.clawsite.ai\",\n    \"sizeBytes\": 458231,\n    \"fileCount\": 12,\n    \"lastDeployAt\": 1744732800\n  }]\n}\n```\n\nUse this to verify your `CLAWSITE_SITE_ID` matches and to inspect current usage vs quotas.\n\n## Other Endpoints\n\n### Delete a site\n\nDELETE $CLAWSITE_API_URL/v1/sites/{siteId}\nAuthorization: Bearer $CLAWSITE_API_KEY\n\nPermanently deletes the site and every file under it. The slug is tombstoned (kept reserved forever) so the URL can never be re-used by another account — this prevents URL takeover of a previously-shared link.\n\n## Standalone Registration (no sandbox env vars)\n\nIf you're running outside a ZenClaw sandbox and `CLAWSITE_API_KEY` isn't pre-set, register via email OTP. Verification is delegated to MBID (MixerBox ID).\n\n**Step 1** — request a 6-digit code via email:\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"email\": \"your-email@example.com\" }\n```\n\n-> Returns: `{ \"challengeId\": \"<JWT>\" }`\n\n**Step 2** — verify (after the 6-digit code arrives in your inbox):\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"challengeId\": \"<JWT from step 1>\", \"code\": \"123456\" }\n```\n\n-> Returns: `accountId`, `apiKey`, and a default `sites[]` entry. **Save the `apiKey` immediately — it cannot be recovered.**\n\nYou can also create additional sites later via:\n\nPOST $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $apiKey\n\n(no body needed; slug is auto-assigned)\n\n> Note: in v1 the per-account site quota is 1, so this returns 409 `quota_exceeded` if you already have one site.\n\n## Quotas (v1)\n\n| Item | Limit |\n|---|---|\n| Sites per account | **1** |\n| Storage per site | **3 MB** (uncompressed total) |\n| Max single file | **1 MB** |\n| Max files per site | **100** |\n| Max compressed zip body | **4 MB** |\n| Deploy frequency | **30 / hour** per account |\n| Purge frequency | **15 / hour** per account |\n| Bandwidth | **unlimited** |\n\n**Allowed file extensions:** `html`, `css`, `js`, `json`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico`, `woff2`, `txt`, `md`.\n\nAnything else (e.g. `.php`, `.exe`, `.py`) → 400 `unsupported_extension`.\n\n## Errors\n\nAll errors return JSON:\n\n```json\n{ \"error\": { \"code\": \"<machine-code>\", \"message\": \"<human-readable>\" } }\n```\n\n| Code | HTTP | Cause |\n|---|---|---|\n| `unauthorized` | 401 | Missing or invalid API key |\n| `missing_fields` | 400 | Required fields absent or malformed (e.g. invalid email shape on register) |\n| `not_found` | 404 | Site doesn't belong to your account, or doesn't exist |\n| `quota_exceeded` | 409 | Sites limit, storage limit, or rate limit hit |\n| `unsupported_extension` | 400 | File extension not in the whitelist above |\n| `file_too_large` | 400 | Single file > 1 MB, or zip body > 4 MB |\n| `invalid_zip` | 400 | Body not a valid zip, missing body, or contains paths with `..` / absolute paths |\n| `mbid_error` | upstream | Forwarded from MBID's email-verify endpoints (`mx_record_not_found`, `domain_typo`, `too_many_request`, `incorrect_verification_code`) — only relevant during email registration |\n| `internal_error` | 500 | Server-side inconsistent state. Should never happen — report if seen. |\n\n**Idempotency note:** `/v1/register` is idempotent on MBID identity. Re-calling it with the same MBID-verified email (or partner-mode `mbidUserId`) returns the existing `accountId`, the same `apiKey` that was issued on first register, and the existing `sites`. There is no rotation API; if you need a fresh key, `DELETE` the site and re-register.\n\nFile v1.0.4:README.md\n\n# ClawSite\n\nAgent-first static website hosting microservice. Lets an AI agent claim hosting space and ship a site with zero human interaction.\n\n- **Production domain:** `clawsite.ai` — API at `api.clawsite.ai`\n- **Development domain:** `dev.clawsite.ai` — API at `api.dev.clawsite.ai`\n- **First partner:** ZenClaw (`MixerBox/zenclaw`)\n- **Skill:** `clawsite-ai` (published to ClawHub)\n- **Sibling service:** [`MixerBox/clawmail`](https://github.com/MixerBox/clawmail) — same operational shape (Terraform, esbuild, single-Lambda + CloudFront-fronted S3, ClawHub-published skill)\n\nSee `CLAUDE.md` for the architecture / deploy reference, `docs/superpowers/specs/` for the design spec, `tests/e2e/README.md` for live-API smoke tests.\n\n## Status\n\n✅ Deployed live to dev + prod. Both AWS account `974718210214` (us-east-1).\n\n| Env | API | Site URL pattern |\n|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` |\n\nZenClaw partner-mode integration is the active code path. Email-OTP registration is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps.\n\nFile v1.0.4:_meta.json\n\n{\n  \"ownerId\": \"kn7e574f4e1ewbqp1xvm49jqss862wx3\",\n  \"slug\": \"clawsite-ai\",\n  \"version\": \"1.0.4\",\n  \"publishedAt\": 1778466958689\n}\n\nFile v1.0.4:CLAUDE.md\n\n# ClawSite\n\nAgent-first static website hosting microservice, modeled after [ClawMail](https://github.com/MixerBox/clawmail).\n\n## Status (2026-04-29)\n\n✅ **v1 deployed to dev + prod, smoke-tested live, partner mode confirmed working end-to-end.**\n\n| Env | API | Site URL pattern | Partner secret on |\n|---|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` | `claws-instance-controller-staging` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` | `claws-instance-controller` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n\nCloudFront distribution IDs: dev `E3VZOWBAWTVYBK`, prod `ESXYWGNPX39MP`.\n\nBoth envs live in AWS account `974718210214` (us-east-1), same account as ClawMail. Route53 zone `clawsite.ai` is shared between dev/prod.\n\n**Email-OTP path is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps in MBID `TABLE_APP`.** Once that's done, set `MBID_SERVER_KEY` on the api Lambda env (Terraform var: `mbid_server_key`) and email registration unlocks. Partner mode (the only path ZenClaw uses today) doesn't depend on this.\n\n## Reference repos\n\n- `~/Work/MixerBox/clawmail/` — the structural model. When in doubt about a pattern (`globalThis.__routes`, Vitest mocks, Terraform), look at how ClawMail does it.\n- `~/Work/MixerBox/zenclaw/` — the first partner. Plan 2 (`configure-clawsite` controller action + sandbox env injection + `clawsite-ai` skill install) lands there.\n- `~/Work/MixerBox/microservice/microservice/kubernetes/livapp/apps/mb-id/` — MBID service we delegate email verification to.\n\n## Quick reference\n\n- **Language:** TypeScript (Node.js 20, ES2022 modules)\n- **Test:** `npx vitest run`\n- **Typecheck:** `npx tsc --noEmit`\n- **Build:** `node build.mjs` (outputs `.mjs` to `dist/`)\n- **Deploy** (Terraform: `terraform/` dir, two workspaces `dev` + `prod`):\n  ```bash\n  # build + zip Lambda artifact (load-bearing filename: api.zip)\n  node build.mjs\n  cd dist && zip -j api.zip api.mjs api.mjs.map\n\n  # dev — uses claws-instance-controller-staging partner secret\n  cd ../terraform && terraform workspace select dev && terraform apply \\\n    -var \"site_domain=dev.clawsite.ai\" \\\n    -var \"api_domain=api.dev.clawsite.ai\" \\\n    -var \"environment=staging\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller-staging \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n\n  # prod — uses claws-instance-controller partner secret\n  terraform workspace select prod && terraform apply \\\n    -var \"site_domain=clawsite.ai\" \\\n    -var \"api_domain=api.clawsite.ai\" \\\n    -var \"environment=prod\" \\\n    -var \"mbid_api_base_url=https://api.id.mixerbox.com\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n  ```\n\n  The `partner_secret` flow: ZenClaw's controller Lambda holds `PARTNER_CLAWSITE_SECRET` as an env var (set once via `aws lambda update-function-configuration`); ClawSite's Terraform reads it at apply time so the two sides cannot drift. Same pattern as ClawMail's `PARTNER_CLAWMAIL_SECRET`.\n- **Hot-patch Lambda code only** (when only src/ changed, no infra): `node build.mjs && cd dist && zip -j api.zip api.mjs api.mjs.map && for fn in clawsite-staging-api clawsite-api; do aws lambda update-function-code --function-name \"$fn\" --zip-file fileb://api.zip; done`\n- **Publish skill to ClawHub:** `clawhub publish .` (run from repo root; `SKILL.md` is the manifest)\n- **Spec:** [`docs/superpowers/specs/2026-04-15-clawsite-design.md`](docs/superpowers/specs/2026-04-15-clawsite-design.md)\n\n### Resource naming convention\n\n`local.prefix` in `terraform/main.tf` derives the AWS resource name prefix from `var.environment`:\n\n- `var.environment = \"prod\"` → `local.prefix = \"clawsite\"` → `clawsite-api`, `clawsite-manifest`, `clawsite-sites-<account>`\n- anything else (typically `staging` for dev workspace) → `local.prefix = \"clawsite-${env}\"` → `clawsite-staging-api`, `clawsite-staging-manifest`, `clawsite-staging-sites-<account>`\n\nThe dev workspace uses `environment=staging` to match ZenClaw's `claws-instance-controller-staging` controller naming. **`-var environment=...` is required on every apply** (no default — pass `prod` for prod, `staging` for dev).\n\n## Architecture (single Lambda)\n\n| Lambda | Trigger | Purpose |\n|---|---|---|\n| `api` | API Gateway HTTP API (`ANY /{proxy+}`) | All REST endpoints — register, sites CRUD, deploy, purge-cache |\n\nThere used to be a `cleanup` Lambda intended for daily housekeeping, but it had no EventBridge trigger and nothing to clean (quota counters self-expire via DDB TTL; slug tombstones are by-design permanent). Removed 2026-04-30. If real housekeeping logic emerges later, re-add the Lambda + EventBridge rule together.\n\nStorage:\n- **DynamoDB** single table `clawsite[-staging]-manifest` — accounts, API keys, sites, slug reservations, quota counters. GSIs: `byApiKeyHash`, `byMbid`.\n- **S3** `clawsite[-staging]-sites-<account-id>` — static site files, partitioned by slug prefix.\n- **CloudFront** distribution + CloudFront Function for `<slug>` host → S3 prefix routing.\n\n## MBID integration (email OTP)\n\nEmail OTP registration delegates to **MBID** (`api.id.{dev.,}mixerbox.com`). ClawSite calls:\n- `POST /api/request_email_verify` → returns a `verifyToken` JWT we pass back as our `challengeId`\n- `PUT /api/verify_email` → returns `user.uuid` which becomes our `mbidUserId`\n\nNet effect: **every ClawSite account is keyed by an MBID UUID**. Partner-mode registration (ZenClaw) and email-OTP registration both deduplicate via the same `byMbid` GSI — one user, one ClawSite account, regardless of how they arrived.\n\nTo activate email-OTP path:\n1. Register `clawsite-dev` and `clawsite-prod` as apps in MBID's `TABLE_APP` to obtain `serverKey` values\n2. Update Lambda env vars: `MBID_SERVER_KEY` (the serverKey for that env) and `MBID_API_BASE_URL` (already set per env)\n\nEmail-OTP code is in place with mocked tests; real serverKey just unlocks the production path.\n\n### Removed from original spec (no longer relevant)\n\nThe spec originally called for self-hosted SES + SQS + a `deliver-email` Lambda. The MBID rework dropped all of that:\n\n- ❌ No SES domain identity / DKIM / SPF / DMARC\n- ❌ No SQS verification queue / DLQ\n- ❌ No `deliver-email` Lambda (architecture went 3 → 2 Lambdas)\n- ❌ No DynamoDB `challenge#*` items (MBID's own `verifyToken` JWT is stateless on our side)\n\nIf re-reading the spec, treat any reference to those items as historical.\n\n## Branching\n\n- `main` — what's deployed to prod (auto-deploy is manual via `terraform apply` on prod workspace)\n- `develop` — staging-equivalent; merges back to `main` after smoke tests\n- Bug fixes / features land via `fix/*` or `feat/*` branches → develop → main\n\n## Project structure\n\n```\nsrc/\n├── handlers/\n│   ├── api.ts              # API Gateway entry; regex route table on globalThis.__routes\n│   └── routes/             # one file per endpoint group, registered via side-effect imports\n│       ├── register.ts\n│       ├── sites.ts\n│       ├── deploy.ts\n│       └── purge.ts\n├── lib/                    # AWS-thin utilities\n│   ├── auth.ts             # SHA-256 + timing-safe apiKey verify + partner secret check\n│   ├── cloudfront.ts       # invalidatePrefix() helper\n│   ├── dynamo.ts           # DDB DocumentClient + PK/SK builders + table/GSI names + hourWindow()\n│   ├── errors.ts           # AppError class, JSON response helpers, CORS headers\n│   ├── id.ts               # acc_, site_, csk_live_* ID generators\n│   ├── mbid.ts             # MBID thin client (requestEmailVerify, verifyEmail)\n│   ├── quota.ts            # Hourly window counters with atomic increments\n│   ├── s3.ts               # S3 client + bucket name\n│   ├── slug.ts             # Random adjective-animal-NN slug + fallback hex slug\n│   ├── validate.ts         # File extension whitelist, size limits, QUOTA constants\n│   └── zip.ts              # Streaming zip extraction with validation\n└── middleware/\n    └── auth.ts             # Bearer apiKey → AuthContext via byApiKeyHash GSI\nbuild.mjs                   # esbuild bundler (entry: api.ts, cleanup.ts)\nterraform/                  # AWS infra; workspaces dev + prod\ntests/                      # Vitest tree mirrors src/\ndocs/superpowers/{specs,plans}/  # Design specs and execution plans\n```\n\n## Key patterns\n\n### Route registration\nRoutes register via side-effect imports in `src/handlers/api.ts`. Each route file calls `route(method, path, handler)` at module level. Routes are stored in `globalThis.__routes` to avoid ESM circular-import TDZ issues — same pattern as ClawMail.\n\nPublic (no-auth) routes: `route('POST', '/path', handler, { public: true })` — currently only `/v1/register`.\n\nThe `route()` function defensively initializes `globalThis.__routes` because esbuild bundling can place dependency module top-level code before the entry's initialization. See comment in `api.ts` for context.\n\n### Auth\n`Authorization: Bearer <token>`:\n- `csk_live_*` → SHA-256 hash → DynamoDB GSI lookup `byApiKeyHash` (per-account API key)\n- Partner secret → on `POST /v1/register` only, matched in handler before falling through to email-mode body shapes\n\n### Single-table DynamoDB\nActive PK/SK prefixes (live in code):\n- `account#<id>` + `meta` / `apikey#<hash>` / `site#<id>` / `quota#<action>#<window>`\n- `slug#<slug>` + `meta` (global slug reservation; status flips to `tombstoned` on site delete and stays forever — prevents URL takeover)\n\n`account#<id>` + `meta` includes a `sitesCount` numeric field, atomically updated by site CRUD operations to enforce the per-account site quota race-free (see `src/handlers/routes/sites.ts`).\n\nGSIs: `byApiKeyHash` (apiKey lookup → accountId), `byMbid` (MBID UUID → accountId for cross-flow idempotency).\n\n### Quota enforcement\n- Per-account site count: atomic via `sitesCount` counter on account meta + conditional update inside the site-creation TransactWrite. Can never race.\n- Per-hour deploy / purge frequency: hourly window counter in `src/lib/quota.ts`, gated on a single conditional `ADD count :one` inside the action handler.\n\n### ClawHub publishing\n`SKILL.md` at the repo root is the agent-facing API doc + ClawHub manifest. Published via `clawhub publish .`. Skill slug: `clawsite-ai`. Once Plan 2 lands, ZenClaw sandboxes will `clawhub install clawsite-ai` during provisioning.\n\n### Testing\nVitest with `vi.mock('@lib/dynamo')`. Route handler tests also mock `@handlers/api` to export a no-op `route` function, avoiding circular imports. Path aliases live in both `tsconfig.json` and `vitest.config.ts` — keep them in sync.\n\n## Known gaps / follow-ups\n\n- **Plan 2 (ZenClaw integration)** is in flight in `~/Work/MixerBox/zenclaw` on `feat/configure-clawsite-action`. Adds the `configure-clawsite` controller action + manifest fields. Sandbox-side env injection + `clawhub install clawsite-ai` are still TODO.\n\nFile v1.0.4:package.json\n\n{\n  \"name\": \"clawsite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Agent-first static website hosting microservice (clawsite.ai)\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"node build.mjs\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:e2e\": \"vitest run --config vitest.e2e.config.ts\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-cloudfront\": \"^3.1007.0\",\n    \"@aws-sdk/client-dynamodb\": \"^3.1007.0\",\n    \"@aws-sdk/client-s3\": \"^3.1007.0\",\n    \"@aws-sdk/lib-dynamodb\": \"^3.1007.0\",\n    \"ulid\": \"^3.0.2\",\n    \"yauzl\": \"^3.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/aws-lambda\": \"^8.10.161\",\n    \"@types/node\": \"^25.4.0\",\n    \"@types/yauzl\": \"^2.10.3\",\n    \"@types/yazl\": \"^3.3.1\",\n    \"esbuild\": \"^0.27.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"yazl\": \"^3.3.1\"\n  }\n}\n\nArchive v1.0.3: 6 files, 11138 bytes\n\nFiles: CLAUDE.md (11336b), package.json (844b), README.md (1202b), SKILL.md (7999b), vitest.e2e.config.ts (1566b), _meta.json (130b)\n\nFile v1.0.3:SKILL.md\n\n---\nname: clawsite-ai\ndescription: >-\n  Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai\n  URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call,\n  atomic full-site replace, automatic CDN cache invalidation. Use when your\n  agent generates static content (portfolios, news pages, link hubs,\n  single-purpose landing pages) and wants to share it as a public URL.\n  Free tier included, no credit card needed.\nversion: 1.0.3\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - CLAWSITE_API_KEY\n        - CLAWSITE_SITE_ID\n    primaryEnv: CLAWSITE_API_KEY\n    homepage: https://clawsite.ai\n---\n\n# Clawsite.ai — Static Website Hosting for AI Agents\n\n## When to Use Clawsite\n\n- Your agent generates static HTML / CSS / JS / images and wants to publish them as a public URL\n- You want a zero-config hosting account with HTTPS, CloudFront CDN, and atomic deploys\n- You want a memorable random URL like `happy-otter-42.clawsite.ai` (the slug is auto-generated; you can't pick it)\n\n## Quick Start\n\n**Your sandbox already has an account provisioned.** Check these env vars before doing anything else:\n\n| Env var | Required? | Purpose |\n|---|---|---|\n| `CLAWSITE_API_KEY` | **required** | Bearer token for all authenticated endpoints (`csk_live_*` format) |\n| `CLAWSITE_SITE_ID` | **required** | Your assigned site identifier (`site_<ulid>` format) |\n| `CLAWSITE_URL` | informational | Your live site URL, the one to share with the user (e.g. `https://happy-otter-42.clawsite.ai`). If unset, derive from `GET /v1/sites`. |\n| `CLAWSITE_API_URL` | optional | API base URL. **Defaults to `https://api.clawsite.ai` if unset.** Dev sandboxes override to `https://api.dev.clawsite.ai`. |\n\nIf `CLAWSITE_API_KEY` or `CLAWSITE_SITE_ID` is unset, see \"Standalone Registration\" at the bottom.\n\n> Examples below use `$CLAWSITE_API_URL` literally; if it's unset, fall back to `https://api.clawsite.ai`.\n\n**API base: `$CLAWSITE_API_URL`/v1**\n\nAll authenticated endpoints require `Authorization: Bearer $CLAWSITE_API_KEY`.\n\n### 1. Deploy a directory of static files\n\nThe deploy endpoint takes a `.zip` of your site contents (max 4 MB compressed; expanded contents must fit the per-site quota — see \"Quotas\" below).\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\nAuthorization: Bearer $CLAWSITE_API_KEY\nContent-Type: application/zip\n\n(body: raw bytes of the .zip)\n\nWorkflow:\n\n1. Create your files in a directory:\n   ```\n   site/\n     index.html\n     style.css\n     app.js\n     images/logo.png\n   ```\n2. Zip the **contents** of the directory (no parent directory inside the zip):\n   ```\n   cd site/ && zip -r ../site.zip .\n   ```\n3. POST the zip:\n   ```\n   curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\"\n   ```\n\n-> Returns: `siteId`, `url`, `fileCount`, `sizeBytes`, `deployedAt` (Unix seconds)\n\n```json\n{\n  \"siteId\": \"site_...\",\n  \"url\": \"https://happy-otter-42.clawsite.ai\",\n  \"fileCount\": 12,\n  \"sizeBytes\": 458231,\n  \"deployedAt\": 1744732800\n}\n```\n\n**Atomic full-site replacement:** any files from the previous deploy that are NOT in the new zip get deleted. Deploy = full snapshot, not incremental upload.\n\n**Cache:** every deploy automatically invalidates CloudFront cache. Manual purge below is rarely needed.\n\n**Routing:** the path inside the zip becomes the URL path. `index.html` at the zip root is served at `/`. Subdirectories work: `images/logo.png` is at `/images/logo.png`. For pretty URLs without `.html`, name files like `about/index.html` and link as `/about/`.\n\n### 2. Show the user their site\n\nThe site is live at `$CLAWSITE_URL` immediately after a successful deploy. Tell the user:\n\n> \"Your site is live at $CLAWSITE_URL\"\n\n### 3. Purge CloudFront cache (rarely needed)\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/purge-cache\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n(no body)\n\n-> Returns: `siteId`, `purgedAt`\n\n```json\n{ \"siteId\": \"site_...\", \"purgedAt\": 1744732800 }\n```\n\nUse this only if the cache is serving stale content unrelated to a deploy. Normal deploys auto-invalidate.\n\n### 4. List sites and check quota usage\n\nGET $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n-> Returns: array of `{ siteId, slug, url, sizeBytes, fileCount, lastDeployAt }`\n\n```json\n{\n  \"sites\": [{\n    \"siteId\": \"site_...\",\n    \"slug\": \"happy-otter-42\",\n    \"url\": \"https://happy-otter-42.clawsite.ai\",\n    \"sizeBytes\": 458231,\n    \"fileCount\": 12,\n    \"lastDeployAt\": 1744732800\n  }]\n}\n```\n\nUse this to verify your `CLAWSITE_SITE_ID` matches and to inspect current usage vs quotas.\n\n## Other Endpoints\n\n### Delete a site\n\nDELETE $CLAWSITE_API_URL/v1/sites/{siteId}\nAuthorization: Bearer $CLAWSITE_API_KEY\n\nPermanently deletes the site and every file under it. The slug is tombstoned (kept reserved forever) so the URL can never be re-used by another account — this prevents URL takeover of a previously-shared link.\n\n## Standalone Registration (no sandbox env vars)\n\nIf you're running outside a ZenClaw sandbox and `CLAWSITE_API_KEY` isn't pre-set, register via email OTP. Verification is delegated to MBID (MixerBox ID).\n\n**Step 1** — request a 6-digit code via email:\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"email\": \"your-email@example.com\" }\n```\n\n-> Returns: `{ \"challengeId\": \"<JWT>\" }`\n\n**Step 2** — verify (after the 6-digit code arrives in your inbox):\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"challengeId\": \"<JWT from step 1>\", \"code\": \"123456\" }\n```\n\n-> Returns: `accountId`, `apiKey`, and a default `sites[]` entry. **Save the `apiKey` immediately — it cannot be recovered.**\n\nYou can also create additional sites later via:\n\nPOST $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $apiKey\n\n(no body needed; slug is auto-assigned)\n\n> Note: in v1 the per-account site quota is 1, so this returns 409 `quota_exceeded` if you already have one site.\n\n## Quotas (v1)\n\n| Item | Limit |\n|---|---|\n| Sites per account | **1** |\n| Storage per site | **3 MB** (uncompressed total) |\n| Max single file | **1 MB** |\n| Max files per site | **100** |\n| Max compressed zip body | **4 MB** |\n| Deploy frequency | **10 / hour** per account |\n| Purge frequency | **5 / hour** per account |\n| Bandwidth | **unlimited** |\n\n**Allowed file extensions:** `html`, `css`, `js`, `json`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico`, `woff2`, `txt`, `md`.\n\nAnything else (e.g. `.php`, `.exe`, `.py`) → 400 `unsupported_extension`.\n\n## Errors\n\nAll errors return JSON:\n\n```json\n{ \"error\": { \"code\": \"<machine-code>\", \"message\": \"<human-readable>\" } }\n```\n\n| Code | HTTP | Cause |\n|---|---|---|\n| `unauthorized` | 401 | Missing or invalid API key |\n| `missing_fields` | 400 | Required fields absent or malformed (e.g. invalid email shape on register) |\n| `not_found` | 404 | Site doesn't belong to your account, or doesn't exist |\n| `quota_exceeded` | 409 | Sites limit, storage limit, or rate limit hit |\n| `unsupported_extension` | 400 | File extension not in the whitelist above |\n| `file_too_large` | 400 | Single file > 1 MB, or zip body > 4 MB |\n| `invalid_zip` | 400 | Body not a valid zip, missing body, or contains paths with `..` / absolute paths |\n| `mbid_error` | upstream | Forwarded from MBID's email-verify endpoints (`mx_record_not_found`, `domain_typo`, `too_many_request`, `incorrect_verification_code`) — only relevant during email registration |\n| `internal_error` | 500 | Server-side inconsistent state. Should never happen — report if seen. |\n\n**Idempotency note:** `/v1/register` is idempotent on MBID identity. Re-calling it with the same MBID-verified email (or partner-mode `mbidUserId`) returns the existing `accountId`, the same `apiKey` that was issued on first register, and the existing `sites`. There is no rotation API; if you need a fresh key, `DELETE` the site and re-register.\n\nFile v1.0.3:README.md\n\n# ClawSite\n\nAgent-first static website hosting microservice. Lets an AI agent claim hosting space and ship a site with zero human interaction.\n\n- **Production domain:** `clawsite.ai` — API at `api.clawsite.ai`\n- **Development domain:** `dev.clawsite.ai` — API at `api.dev.clawsite.ai`\n- **First partner:** ZenClaw (`MixerBox/zenclaw`)\n- **Skill:** `clawsite-ai` (published to ClawHub)\n- **Sibling service:** [`MixerBox/clawmail`](https://github.com/MixerBox/clawmail) — same operational shape (Terraform, esbuild, single-Lambda + CloudFront-fronted S3, ClawHub-published skill)\n\nSee `CLAUDE.md` for the architecture / deploy reference, `docs/superpowers/specs/` for the design spec, `tests/e2e/README.md` for live-API smoke tests.\n\n## Status\n\n✅ Deployed live to dev + prod. Both AWS account `974718210214` (us-east-1).\n\n| Env | API | Site URL pattern |\n|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` |\n\nZenClaw partner-mode integration is the active code path. Email-OTP registration is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps.\n\nFile v1.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn7e574f4e1ewbqp1xvm49jqss862wx3\",\n  \"slug\": \"clawsite-ai\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1778120278301\n}\n\nFile v1.0.3:CLAUDE.md\n\n# ClawSite\n\nAgent-first static website hosting microservice, modeled after [ClawMail](https://github.com/MixerBox/clawmail).\n\n## Status (2026-04-29)\n\n✅ **v1 deployed to dev + prod, smoke-tested live, partner mode confirmed working end-to-end.**\n\n| Env | API | Site URL pattern | Partner secret on |\n|---|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` | `claws-instance-controller-staging` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` | `claws-instance-controller` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n\nCloudFront distribution IDs: dev `E3VZOWBAWTVYBK`, prod `ESXYWGNPX39MP`.\n\nBoth envs live in AWS account `974718210214` (us-east-1), same account as ClawMail. Route53 zone `clawsite.ai` is shared between dev/prod.\n\n**Email-OTP path is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps in MBID `TABLE_APP`.** Once that's done, set `MBID_SERVER_KEY` on the api Lambda env (Terraform var: `mbid_server_key`) and email registration unlocks. Partner mode (the only path ZenClaw uses today) doesn't depend on this.\n\n## Reference repos\n\n- `~/Work/MixerBox/clawmail/` — the structural model. When in doubt about a pattern (`globalThis.__routes`, Vitest mocks, Terraform), look at how ClawMail does it.\n- `~/Work/MixerBox/zenclaw/` — the first partner. Plan 2 (`configure-clawsite` controller action + sandbox env injection + `clawsite-ai` skill install) lands there.\n- `~/Work/MixerBox/microservice/microservice/kubernetes/livapp/apps/mb-id/` — MBID service we delegate email verification to.\n\n## Quick reference\n\n- **Language:** TypeScript (Node.js 20, ES2022 modules)\n- **Test:** `npx vitest run`\n- **Typecheck:** `npx tsc --noEmit`\n- **Build:** `node build.mjs` (outputs `.mjs` to `dist/`)\n- **Deploy** (Terraform: `terraform/` dir, two workspaces `dev` + `prod`):\n  ```bash\n  # build + zip Lambda artifact (load-bearing filename: api.zip)\n  node build.mjs\n  cd dist && zip -j api.zip api.mjs api.mjs.map\n\n  # dev — uses claws-instance-controller-staging partner secret\n  cd ../terraform && terraform workspace select dev && terraform apply \\\n    -var \"site_domain=dev.clawsite.ai\" \\\n    -var \"api_domain=api.dev.clawsite.ai\" \\\n    -var \"environment=staging\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller-staging \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n\n  # prod — uses claws-instance-controller partner secret\n  terraform workspace select prod && terraform apply \\\n    -var \"site_domain=clawsite.ai\" \\\n    -var \"api_domain=api.clawsite.ai\" \\\n    -var \"environment=prod\" \\\n    -var \"mbid_api_base_url=https://api.id.mixerbox.com\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n  ```\n\n  The `partner_secret` flow: ZenClaw's controller Lambda holds `PARTNER_CLAWSITE_SECRET` as an env var (set once via `aws lambda update-function-configuration`); ClawSite's Terraform reads it at apply time so the two sides cannot drift. Same pattern as ClawMail's `PARTNER_CLAWMAIL_SECRET`.\n- **Hot-patch Lambda code only** (when only src/ changed, no infra): `node build.mjs && cd dist && zip -j api.zip api.mjs api.mjs.map && for fn in clawsite-staging-api clawsite-api; do aws lambda update-function-code --function-name \"$fn\" --zip-file fileb://api.zip; done`\n- **Publish skill to ClawHub:** `clawhub publish .` (run from repo root; `SKILL.md` is the manifest)\n- **Spec:** [`docs/superpowers/specs/2026-04-15-clawsite-design.md`](docs/superpowers/specs/2026-04-15-clawsite-design.md)\n\n### Resource naming convention\n\n`local.prefix` in `terraform/main.tf` derives the AWS resource name prefix from `var.environment`:\n\n- `var.environment = \"prod\"` → `local.prefix = \"clawsite\"` → `clawsite-api`, `clawsite-manifest`, `clawsite-sites-<account>`\n- anything else (typically `staging` for dev workspace) → `local.prefix = \"clawsite-${env}\"` → `clawsite-staging-api`, `clawsite-staging-manifest`, `clawsite-staging-sites-<account>`\n\nThe dev workspace uses `environment=staging` to match ZenClaw's `claws-instance-controller-staging` controller naming. **`-var environment=...` is required on every apply** (no default — pass `prod` for prod, `staging` for dev).\n\n## Architecture (single Lambda)\n\n| Lambda | Trigger | Purpose |\n|---|---|---|\n| `api` | API Gateway HTTP API (`ANY /{proxy+}`) | All REST endpoints — register, sites CRUD, deploy, purge-cache |\n\nThere used to be a `cleanup` Lambda intended for daily housekeeping, but it had no EventBridge trigger and nothing to clean (quota counters self-expire via DDB TTL; slug tombstones are by-design permanent). Removed 2026-04-30. If real housekeeping logic emerges later, re-add the Lambda + EventBridge rule together.\n\nStorage:\n- **DynamoDB** single table `clawsite[-staging]-manifest` — accounts, API keys, sites, slug reservations, quota counters. GSIs: `byApiKeyHash`, `byMbid`.\n- **S3** `clawsite[-staging]-sites-<account-id>` — static site files, partitioned by slug prefix.\n- **CloudFront** distribution + CloudFront Function for `<slug>` host → S3 prefix routing.\n\n## MBID integration (email OTP)\n\nEmail OTP registration delegates to **MBID** (`api.id.{dev.,}mixerbox.com`). ClawSite calls:\n- `POST /api/request_email_verify` → returns a `verifyToken` JWT we pass back as our `challengeId`\n- `PUT /api/verify_email` → returns `user.uuid` which becomes our `mbidUserId`\n\nNet effect: **every ClawSite account is keyed by an MBID UUID**. Partner-mode registration (ZenClaw) and email-OTP registration both deduplicate via the same `byMbid` GSI — one user, one ClawSite account, regardless of how they arrived.\n\nTo activate email-OTP path:\n1. Register `clawsite-dev` and `clawsite-prod` as apps in MBID's `TABLE_APP` to obtain `serverKey` values\n2. Update Lambda env vars: `MBID_SERVER_KEY` (the serverKey for that env) and `MBID_API_BASE_URL` (already set per env)\n\nEmail-OTP code is in place with mocked tests; real serverKey just unlocks the production path.\n\n### Removed from original spec (no longer relevant)\n\nThe spec originally called for self-hosted SES + SQS + a `deliver-email` Lambda. The MBID rework dropped all of that:\n\n- ❌ No SES domain identity / DKIM / SPF / DMARC\n- ❌ No SQS verification queue / DLQ\n- ❌ No `deliver-email` Lambda (architecture went 3 → 2 Lambdas)\n- ❌ No DynamoDB `challenge#*` items (MBID's own `verifyToken` JWT is stateless on our side)\n\nIf re-reading the spec, treat any reference to those items as historical.\n\n## Branching\n\n- `main` — what's deployed to prod (auto-deploy is manual via `terraform apply` on prod workspace)\n- `develop` — staging-equivalent; merges back to `main` after smoke tests\n- Bug fixes / features land via `fix/*` or `feat/*` branches → develop → main\n\n## Project structure\n\n```\nsrc/\n├── handlers/\n│   ├── api.ts              # API Gateway entry; regex route table on globalThis.__routes\n│   └── routes/             # one file per endpoint group, registered via side-effect imports\n│       ├── register.ts\n│       ├── sites.ts\n│       ├── deploy.ts\n│       └── purge.ts\n├── lib/                    # AWS-thin utilities\n│   ├── auth.ts             # SHA-256 + timing-safe apiKey verify + partner secret check\n│   ├── cloudfront.ts       # invalidatePrefix() helper\n│   ├── dynamo.ts           # DDB DocumentClient + PK/SK builders + table/GSI names + hourWindow()\n│   ├── errors.ts           # AppError class, JSON response helpers, CORS headers\n│   ├── id.ts               # acc_, site_, csk_live_* ID generators\n│   ├── mbid.ts             # MBID thin client (requestEmailVerify, verifyEmail)\n│   ├── quota.ts            # Hourly window counters with atomic increments\n│   ├── s3.ts               # S3 client + bucket name\n│   ├── slug.ts             # Random adjective-animal-NN slug + fallback hex slug\n│   ├── validate.ts         # File extension whitelist, size limits, QUOTA constants\n│   └── zip.ts              # Streaming zip extraction with validation\n└── middleware/\n    └── auth.ts             # Bearer apiKey → AuthContext via byApiKeyHash GSI\nbuild.mjs                   # esbuild bundler (entry: api.ts, cleanup.ts)\nterraform/                  # AWS infra; workspaces dev + prod\ntests/                      # Vitest tree mirrors src/\ndocs/superpowers/{specs,plans}/  # Design specs and execution plans\n```\n\n## Key patterns\n\n### Route registration\nRoutes register via side-effect imports in `src/handlers/api.ts`. Each route file calls `route(method, path, handler)` at module level. Routes are stored in `globalThis.__routes` to avoid ESM circular-import TDZ issues — same pattern as ClawMail.\n\nPublic (no-auth) routes: `route('POST', '/path', handler, { public: true })` — currently only `/v1/register`.\n\nThe `route()` function defensively initializes `globalThis.__routes` because esbuild bundling can place dependency module top-level code before the entry's initialization. See comment in `api.ts` for context.\n\n### Auth\n`Authorization: Bearer <token>`:\n- `csk_live_*` → SHA-256 hash → DynamoDB GSI lookup `byApiKeyHash` (per-account API key)\n- Partner secret → on `POST /v1/register` only, matched in handler before falling through to email-mode body shapes\n\n### Single-table DynamoDB\nActive PK/SK prefixes (live in code):\n- `account#<id>` + `meta` / `apikey#<hash>` / `site#<id>` / `quota#<action>#<window>`\n- `slug#<slug>` + `meta` (global slug reservation; status flips to `tombstoned` on site delete and stays forever — prevents URL takeover)\n\n`account#<id>` + `meta` includes a `sitesCount` numeric field, atomically updated by site CRUD operations to enforce the per-account site quota race-free (see `src/handlers/routes/sites.ts`).\n\nGSIs: `byApiKeyHash` (apiKey lookup → accountId), `byMbid` (MBID UUID → accountId for cross-flow idempotency).\n\n### Quota enforcement\n- Per-account site count: atomic via `sitesCount` counter on account meta + conditional update inside the site-creation TransactWrite. Can never race.\n- Per-hour deploy / purge frequency: hourly window counter in `src/lib/quota.ts`, gated on a single conditional `ADD count :one` inside the action handler.\n\n### ClawHub publishing\n`SKILL.md` at the repo root is the agent-facing API doc + ClawHub manifest. Published via `clawhub publish .`. Skill slug: `clawsite-ai`. Once Plan 2 lands, ZenClaw sandboxes will `clawhub install clawsite-ai` during provisioning.\n\n### Testing\nVitest with `vi.mock('@lib/dynamo')`. Route handler tests also mock `@handlers/api` to export a no-op `route` function, avoiding circular imports. Path aliases live in both `tsconfig.json` and `vitest.config.ts` — keep them in sync.\n\n## Known gaps / follow-ups\n\n- **Plan 2 (ZenClaw integration)** is in flight in `~/Work/MixerBox/zenclaw` on `feat/configure-clawsite-action`. Adds the `configure-clawsite` controller action + manifest fields. Sandbox-side env injection + `clawhub install clawsite-ai` are still TODO.\n\nFile v1.0.3:package.json\n\n{\n  \"name\": \"clawsite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Agent-first static website hosting microservice (clawsite.ai)\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"node build.mjs\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:e2e\": \"vitest run --config vitest.e2e.config.ts\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-cloudfront\": \"^3.1007.0\",\n    \"@aws-sdk/client-dynamodb\": \"^3.1007.0\",\n    \"@aws-sdk/client-s3\": \"^3.1007.0\",\n    \"@aws-sdk/lib-dynamodb\": \"^3.1007.0\",\n    \"ulid\": \"^3.0.2\",\n    \"yauzl\": \"^3.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/aws-lambda\": \"^8.10.161\",\n    \"@types/node\": \"^25.4.0\",\n    \"@types/yauzl\": \"^2.10.3\",\n    \"@types/yazl\": \"^3.3.1\",\n    \"esbuild\": \"^0.27.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"yazl\": \"^3.3.1\"\n  }\n}\n\nArchive v1.0.2: 5 files, 9819 bytes\n\nFiles: CLAUDE.md (11336b), package.json (784b), README.md (629b), SKILL.md (7897b), _meta.json (130b)\n\nFile v1.0.2:SKILL.md\n\n---\nname: clawsite-ai\ndescription: >-\n  Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai\n  URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call,\n  atomic full-site replace, automatic CDN cache invalidation. Use when your\n  agent generates static content (portfolios, news pages, link hubs,\n  single-purpose landing pages) and wants to share it as a public URL.\n  Free tier included, no credit card needed.\nversion: 1.0.2\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - CLAWSITE_API_KEY\n        - CLAWSITE_SITE_ID\n    primaryEnv: CLAWSITE_API_KEY\n    homepage: https://clawsite.ai\n---\n\n# Clawsite.ai — Static Website Hosting for AI Agents\n\n## When to Use Clawsite\n\n- Your agent generates static HTML / CSS / JS / images and wants to publish them as a public URL\n- You want a zero-config hosting account with HTTPS, CloudFront CDN, and atomic deploys\n- You want a memorable random URL like `happy-otter-42.clawsite.ai` (the slug is auto-generated; you can't pick it)\n\n## Quick Start\n\n**Your sandbox already has an account provisioned.** Check these env vars before doing anything else:\n\n| Env var | Required? | Purpose |\n|---|---|---|\n| `CLAWSITE_API_KEY` | **required** | Bearer token for all authenticated endpoints (e.g. `csk_live_...`) |\n| `CLAWSITE_SITE_ID` | **required** | Your assigned site identifier (e.g. `site_01KQ...`) |\n| `CLAWSITE_URL` | informational | Your live site URL, the one to share with the user (e.g. `https://happy-otter-42.clawsite.ai`). If unset, derive from `GET /v1/sites`. |\n| `CLAWSITE_API_URL` | optional | API base URL. **Defaults to `https://api.clawsite.ai` if unset.** Dev sandboxes override to `https://api.dev.clawsite.ai`. |\n\nIf `CLAWSITE_API_KEY` or `CLAWSITE_SITE_ID` is unset, see \"Standalone Registration\" at the bottom.\n\n> Examples below use `$CLAWSITE_API_URL` literally; if it's unset, fall back to `https://api.clawsite.ai`.\n\n**API base: `$CLAWSITE_API_URL`/v1**\n\nAll authenticated endpoints require `Authorization: Bearer $CLAWSITE_API_KEY`.\n\n### 1. Deploy a directory of static files\n\nThe deploy endpoint takes a `.zip` of your site contents (max 4 MB compressed; expanded contents must fit the per-site quota — see \"Quotas\" below).\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\nAuthorization: Bearer $CLAWSITE_API_KEY\nContent-Type: application/zip\n\n(body: raw bytes of the .zip)\n\nWorkflow:\n\n1. Create your files in a directory:\n   ```\n   site/\n     index.html\n     style.css\n     app.js\n     images/logo.png\n   ```\n2. Zip the **contents** of the directory (no parent directory inside the zip):\n   ```\n   cd site/ && zip -r ../site.zip .\n   ```\n3. POST the zip:\n   ```\n   curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\"\n   ```\n\n-> Returns: `siteId`, `url`, `fileCount`, `sizeBytes`, `deployedAt` (Unix seconds)\n\n```json\n{\n  \"siteId\": \"site_...\",\n  \"url\": \"https://happy-otter-42.clawsite.ai\",\n  \"fileCount\": 12,\n  \"sizeBytes\": 458231,\n  \"deployedAt\": 1744732800\n}\n```\n\n**Atomic full-site replacement:** any files from the previous deploy that are NOT in the new zip get deleted. Deploy = full snapshot, not incremental upload.\n\n**Cache:** every deploy automatically invalidates CloudFront cache. Manual purge below is rarely needed.\n\n**Routing:** the path inside the zip becomes the URL path. `index.html` at the zip root is served at `/`. Subdirectories work: `images/logo.png` is at `/images/logo.png`. For pretty URLs without `.html`, name files like `about/index.html` and link as `/about/`.\n\n### 2. Show the user their site\n\nThe site is live at `$CLAWSITE_URL` immediately after a successful deploy. Tell the user:\n\n> \"Your site is live at $CLAWSITE_URL\"\n\n### 3. Purge CloudFront cache (rarely needed)\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/purge-cache\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n(no body)\n\n-> Returns: `siteId`, `purgedAt`\n\n```json\n{ \"siteId\": \"site_...\", \"purgedAt\": 1744732800 }\n```\n\nUse this only if the cache is serving stale content unrelated to a deploy. Normal deploys auto-invalidate.\n\n### 4. List sites and check quota usage\n\nGET $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n-> Returns: array of `{ siteId, slug, url, sizeBytes, fileCount, lastDeployAt }`\n\n```json\n{\n  \"sites\": [{\n    \"siteId\": \"site_...\",\n    \"slug\": \"happy-otter-42\",\n    \"url\": \"https://happy-otter-42.clawsite.ai\",\n    \"sizeBytes\": 458231,\n    \"fileCount\": 12,\n    \"lastDeployAt\": 1744732800\n  }]\n}\n```\n\nUse this to verify your `CLAWSITE_SITE_ID` matches and to inspect current usage vs quotas.\n\n## Other Endpoints\n\n### Delete a site\n\nDELETE $CLAWSITE_API_URL/v1/sites/{siteId}\nAuthorization: Bearer $CLAWSITE_API_KEY\n\nPermanently deletes the site and every file under it. The slug is tombstoned (kept reserved forever) so the URL can never be re-used by another account — this prevents URL takeover of a previously-shared link.\n\n## Standalone Registration (no sandbox env vars)\n\nIf you're running outside a ZenClaw sandbox and `CLAWSITE_API_KEY` isn't pre-set, register via email OTP. Verification is delegated to MBID (MixerBox ID).\n\n**Step 1** — request a 6-digit code via email:\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"email\": \"your-email@example.com\" }\n```\n\n-> Returns: `{ \"challengeId\": \"<JWT>\" }`\n\n**Step 2** — verify (after the 6-digit code arrives in your inbox):\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"challengeId\": \"<JWT from step 1>\", \"code\": \"123456\" }\n```\n\n-> Returns: `accountId`, `apiKey`, and a default `sites[]` entry. **Save the `apiKey` immediately — it cannot be recovered.**\n\nYou can also create additional sites later via:\n\nPOST $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $apiKey\n\n(no body needed; slug is auto-assigned)\n\n> Note: in v1 the per-account site quota is 1, so this returns 409 `quota_exceeded` if you already have one site.\n\n## Quotas (v1)\n\n| Item | Limit |\n|---|---|\n| Sites per account | **1** |\n| Storage per site | **3 MB** (uncompressed total) |\n| Max single file | **1 MB** |\n| Max files per site | **100** |\n| Max compressed zip body | **4 MB** |\n| Deploy frequency | **10 / hour** per account |\n| Purge frequency | **5 / hour** per account |\n| Bandwidth | **unlimited** |\n\n**Allowed file extensions:** `html`, `css`, `js`, `json`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico`, `woff2`, `txt`, `md`.\n\nAnything else (e.g. `.php`, `.exe`, `.py`) → 400 `unsupported_extension`.\n\n## Errors\n\nAll errors return JSON:\n\n```json\n{ \"error\": { \"code\": \"<machine-code>\", \"message\": \"<human-readable>\" } }\n```\n\n| Code | HTTP | Cause |\n|---|---|---|\n| `unauthorized` | 401 | Missing or invalid API key |\n| `missing_fields` | 400 | Required fields absent or malformed (e.g. invalid email shape on register) |\n| `not_found` | 404 | Site doesn't belong to your account, or doesn't exist |\n| `quota_exceeded` | 409 | Sites limit, storage limit, or rate limit hit |\n| `unsupported_extension` | 400 | File extension not in the whitelist above |\n| `file_too_large` | 400 | Single file > 1 MB, or zip body > 4 MB |\n| `invalid_zip` | 400 | Body not a valid zip, missing body, or contains paths with `..` / absolute paths |\n| `mbid_error` | upstream | Forwarded from MBID's email-verify endpoints (`mx_record_not_found`, `domain_typo`, `too_many_request`, `incorrect_verification_code`) — only relevant during email registration |\n\n**Idempotency note:** `/v1/register` is idempotent on MBID identity. Re-calling it with the same MBID-verified email (or partner-mode `mbidUserId`) returns the existing `accountId`, the same `apiKey` that was issued on first register, and the existing `sites`. There is no rotation API; if you need a fresh key, `DELETE` the site and re-register.\n\nFile v1.0.2:README.md\n\n# ClawSite\n\nAgent-first static website hosting microservice. Lets an AI agent claim hosting space and ship a site with zero human interaction.\n\n- **Production domain:** `clawsite.ai`\n- **Development domain:** `dev.clawsite.ai`\n- **First partner:** ZenClaw (`MixerBox/zenclaw`)\n- **Sibling service:** [`MixerBox/clawmail`](https://github.com/MixerBox/clawmail) — same operational shape (Terraform, esbuild, multi-Lambda, ClawHub-published skill)\n\nSee `CLAUDE.md` for quick architecture reference and `docs/superpowers/specs/` for the v1 design spec.\n\n## Status\n\n🚧 v1 implementation in progress. Repo bootstrapped 2026-04-17.\n\nFile v1.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn7e574f4e1ewbqp1xvm49jqss862wx3\",\n  \"slug\": \"clawsite-ai\",\n  \"version\": \"1.0.2\",\n  \"publishedAt\": 1777897045085\n}\n\nFile v1.0.2:CLAUDE.md\n\n# ClawSite\n\nAgent-first static website hosting microservice, modeled after [ClawMail](https://github.com/MixerBox/clawmail).\n\n## Status (2026-04-29)\n\n✅ **v1 deployed to dev + prod, smoke-tested live, partner mode confirmed working end-to-end.**\n\n| Env | API | Site URL pattern | Partner secret on |\n|---|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` | `claws-instance-controller-staging` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` | `claws-instance-controller` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n\nCloudFront distribution IDs: dev `E3VZOWBAWTVYBK`, prod `ESXYWGNPX39MP`.\n\nBoth envs live in AWS account `974718210214` (us-east-1), same account as ClawMail. Route53 zone `clawsite.ai` is shared between dev/prod.\n\n**Email-OTP path is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps in MBID `TABLE_APP`.** Once that's done, set `MBID_SERVER_KEY` on the api Lambda env (Terraform var: `mbid_server_key`) and email registration unlocks. Partner mode (the only path ZenClaw uses today) doesn't depend on this.\n\n## Reference repos\n\n- `~/Work/MixerBox/clawmail/` — the structural model. When in doubt about a pattern (`globalThis.__routes`, Vitest mocks, Terraform), look at how ClawMail does it.\n- `~/Work/MixerBox/zenclaw/` — the first partner. Plan 2 (`configure-clawsite` controller action + sandbox env injection + `clawsite-ai` skill install) lands there.\n- `~/Work/MixerBox/microservice/microservice/kubernetes/livapp/apps/mb-id/` — MBID service we delegate email verification to.\n\n## Quick reference\n\n- **Language:** TypeScript (Node.js 20, ES2022 modules)\n- **Test:** `npx vitest run`\n- **Typecheck:** `npx tsc --noEmit`\n- **Build:** `node build.mjs` (outputs `.mjs` to `dist/`)\n- **Deploy** (Terraform: `terraform/` dir, two workspaces `dev` + `prod`):\n  ```bash\n  # build + zip Lambda artifact (load-bearing filename: api.zip)\n  node build.mjs\n  cd dist && zip -j api.zip api.mjs api.mjs.map\n\n  # dev — uses claws-instance-controller-staging partner secret\n  cd ../terraform && terraform workspace select dev && terraform apply \\\n    -var \"site_domain=dev.clawsite.ai\" \\\n    -var \"api_domain=api.dev.clawsite.ai\" \\\n    -var \"environment=staging\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller-staging \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n\n  # prod — uses claws-instance-controller partner secret\n  terraform workspace select prod && terraform apply \\\n    -var \"site_domain=clawsite.ai\" \\\n    -var \"api_domain=api.clawsite.ai\" \\\n    -var \"environment=prod\" \\\n    -var \"mbid_api_base_url=https://api.id.mixerbox.com\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n  ```\n\n  The `partner_secret` flow: ZenClaw's controller Lambda holds `PARTNER_CLAWSITE_SECRET` as an env var (set once via `aws lambda update-function-configuration`); ClawSite's Terraform reads it at apply time so the two sides cannot drift. Same pattern as ClawMail's `PARTNER_CLAWMAIL_SECRET`.\n- **Hot-patch Lambda code only** (when only src/ changed, no infra): `node build.mjs && cd dist && zip -j api.zip api.mjs api.mjs.map && for fn in clawsite-staging-api clawsite-api; do aws lambda update-function-code --function-name \"$fn\" --zip-file fileb://api.zip; done`\n- **Publish skill to ClawHub:** `clawhub publish .` (run from repo root; `SKILL.md` is the manifest)\n- **Spec:** [`docs/superpowers/specs/2026-04-15-clawsite-design.md`](docs/superpowers/specs/2026-04-15-clawsite-design.md)\n\n### Resource naming convention\n\n`local.prefix` in `terraform/main.tf` derives the AWS resource name prefix from `var.environment`:\n\n- `var.environment = \"prod\"` → `local.prefix = \"clawsite\"` → `clawsite-api`, `clawsite-manifest`, `clawsite-sites-<account>`\n- anything else (typically `staging` for dev workspace) → `local.prefix = \"clawsite-${env}\"` → `clawsite-staging-api`, `clawsite-staging-manifest`, `clawsite-staging-sites-<account>`\n\nThe dev workspace uses `environment=staging` to match ZenClaw's `claws-instance-controller-staging` controller naming. **`-var environment=...` is required on every apply** (no default — pass `prod` for prod, `staging` for dev).\n\n## Architecture (single Lambda)\n\n| Lambda | Trigger | Purpose |\n|---|---|---|\n| `api` | API Gateway HTTP API (`ANY /{proxy+}`) | All REST endpoints — register, sites CRUD, deploy, purge-cache |\n\nThere used to be a `cleanup` Lambda intended for daily housekeeping, but it had no EventBridge trigger and nothing to clean (quota counters self-expire via DDB TTL; slug tombstones are by-design permanent). Removed 2026-04-30. If real housekeeping logic emerges later, re-add the Lambda + EventBridge rule together.\n\nStorage:\n- **DynamoDB** single table `clawsite[-staging]-manifest` — accounts, API keys, sites, slug reservations, quota counters. GSIs: `byApiKeyHash`, `byMbid`.\n- **S3** `clawsite[-staging]-sites-<account-id>` — static site files, partitioned by slug prefix.\n- **CloudFront** distribution + CloudFront Function for `<slug>` host → S3 prefix routing.\n\n## MBID integration (email OTP)\n\nEmail OTP registration delegates to **MBID** (`api.id.{dev.,}mixerbox.com`). ClawSite calls:\n- `POST /api/request_email_verify` → returns a `verifyToken` JWT we pass back as our `challengeId`\n- `PUT /api/verify_email` → returns `user.uuid` which becomes our `mbidUserId`\n\nNet effect: **every ClawSite account is keyed by an MBID UUID**. Partner-mode registration (ZenClaw) and email-OTP registration both deduplicate via the same `byMbid` GSI — one user, one ClawSite account, regardless of how they arrived.\n\nTo activate email-OTP path:\n1. Register `clawsite-dev` and `clawsite-prod` as apps in MBID's `TABLE_APP` to obtain `serverKey` values\n2. Update Lambda env vars: `MBID_SERVER_KEY` (the serverKey for that env) and `MBID_API_BASE_URL` (already set per env)\n\nEmail-OTP code is in place with mocked tests; real serverKey just unlocks the production path.\n\n### Removed from original spec (no longer relevant)\n\nThe spec originally called for self-hosted SES + SQS + a `deliver-email` Lambda. The MBID rework dropped all of that:\n\n- ❌ No SES domain identity / DKIM / SPF / DMARC\n- ❌ No SQS verification queue / DLQ\n- ❌ No `deliver-email` Lambda (architecture went 3 → 2 Lambdas)\n- ❌ No DynamoDB `challenge#*` items (MBID's own `verifyToken` JWT is stateless on our side)\n\nIf re-reading the spec, treat any reference to those items as historical.\n\n## Branching\n\n- `main` — what's deployed to prod (auto-deploy is manual via `terraform apply` on prod workspace)\n- `develop` — staging-equivalent; merges back to `main` after smoke tests\n- Bug fixes / features land via `fix/*` or `feat/*` branches → develop → main\n\n## Project structure\n\n```\nsrc/\n├── handlers/\n│   ├── api.ts              # API Gateway entry; regex route table on globalThis.__routes\n│   └── routes/             # one file per endpoint group, registered via side-effect imports\n│       ├── register.ts\n│       ├── sites.ts\n│       ├── deploy.ts\n│       └── purge.ts\n├── lib/                    # AWS-thin utilities\n│   ├── auth.ts             # SHA-256 + timing-safe apiKey verify + partner secret check\n│   ├── cloudfront.ts       # invalidatePrefix() helper\n│   ├── dynamo.ts           # DDB DocumentClient + PK/SK builders + table/GSI names + hourWindow()\n│   ├── errors.ts           # AppError class, JSON response helpers, CORS headers\n│   ├── id.ts               # acc_, site_, csk_live_* ID generators\n│   ├── mbid.ts             # MBID thin client (requestEmailVerify, verifyEmail)\n│   ├── quota.ts            # Hourly window counters with atomic increments\n│   ├── s3.ts               # S3 client + bucket name\n│   ├── slug.ts             # Random adjective-animal-NN slug + fallback hex slug\n│   ├── validate.ts         # File extension whitelist, size limits, QUOTA constants\n│   └── zip.ts              # Streaming zip extraction with validation\n└── middleware/\n    └── auth.ts             # Bearer apiKey → AuthContext via byApiKeyHash GSI\nbuild.mjs                   # esbuild bundler (entry: api.ts, cleanup.ts)\nterraform/                  # AWS infra; workspaces dev + prod\ntests/                      # Vitest tree mirrors src/\ndocs/superpowers/{specs,plans}/  # Design specs and execution plans\n```\n\n## Key patterns\n\n### Route registration\nRoutes register via side-effect imports in `src/handlers/api.ts`. Each route file calls `route(method, path, handler)` at module level. Routes are stored in `globalThis.__routes` to avoid ESM circular-import TDZ issues — same pattern as ClawMail.\n\nPublic (no-auth) routes: `route('POST', '/path', handler, { public: true })` — currently only `/v1/register`.\n\nThe `route()` function defensively initializes `globalThis.__routes` because esbuild bundling can place dependency module top-level code before the entry's initialization. See comment in `api.ts` for context.\n\n### Auth\n`Authorization: Bearer <token>`:\n- `csk_live_*` → SHA-256 hash → DynamoDB GSI lookup `byApiKeyHash` (per-account API key)\n- Partner secret → on `POST /v1/register` only, matched in handler before falling through to email-mode body shapes\n\n### Single-table DynamoDB\nActive PK/SK prefixes (live in code):\n- `account#<id>` + `meta` / `apikey#<hash>` / `site#<id>` / `quota#<action>#<window>`\n- `slug#<slug>` + `meta` (global slug reservation; status flips to `tombstoned` on site delete and stays forever — prevents URL takeover)\n\n`account#<id>` + `meta` includes a `sitesCount` numeric field, atomically updated by site CRUD operations to enforce the per-account site quota race-free (see `src/handlers/routes/sites.ts`).\n\nGSIs: `byApiKeyHash` (apiKey lookup → accountId), `byMbid` (MBID UUID → accountId for cross-flow idempotency).\n\n### Quota enforcement\n- Per-account site count: atomic via `sitesCount` counter on account meta + conditional update inside the site-creation TransactWrite. Can never race.\n- Per-hour deploy / purge frequency: hourly window counter in `src/lib/quota.ts`, gated on a single conditional `ADD count :one` inside the action handler.\n\n### ClawHub publishing\n`SKILL.md` at the repo root is the agent-facing API doc + ClawHub manifest. Published via `clawhub publish .`. Skill slug: `clawsite-ai`. Once Plan 2 lands, ZenClaw sandboxes will `clawhub install clawsite-ai` during provisioning.\n\n### Testing\nVitest with `vi.mock('@lib/dynamo')`. Route handler tests also mock `@handlers/api` to export a no-op `route` function, avoiding circular imports. Path aliases live in both `tsconfig.json` and `vitest.config.ts` — keep them in sync.\n\n## Known gaps / follow-ups\n\n- **Plan 2 (ZenClaw integration)** is in flight in `~/Work/MixerBox/zenclaw` on `feat/configure-clawsite-action`. Adds the `configure-clawsite` controller action + manifest fields. Sandbox-side env injection + `clawhub install clawsite-ai` are still TODO.\n\nFile v1.0.2:package.json\n\n{\n  \"name\": \"clawsite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Agent-first static website hosting microservice (clawsite.ai)\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"node build.mjs\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-cloudfront\": \"^3.1007.0\",\n    \"@aws-sdk/client-dynamodb\": \"^3.1007.0\",\n    \"@aws-sdk/client-s3\": \"^3.1007.0\",\n    \"@aws-sdk/lib-dynamodb\": \"^3.1007.0\",\n    \"ulid\": \"^3.0.2\",\n    \"yauzl\": \"^3.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/aws-lambda\": \"^8.10.161\",\n    \"@types/node\": \"^25.4.0\",\n    \"@types/yauzl\": \"^2.10.3\",\n    \"@types/yazl\": \"^3.3.1\",\n    \"esbuild\": \"^0.27.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"yazl\": \"^3.3.1\"\n  }\n}\n\nArchive v1.0.1: 5 files, 9822 bytes\n\nFiles: CLAUDE.md (11365b), package.json (784b), README.md (629b), SKILL.md (7904b), _meta.json (130b)\n\nFile v1.0.1:SKILL.md\n\n---\nname: clawsite-ai\ndescription: >-\n  Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai\n  URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call,\n  atomic full-site replace, automatic CDN cache invalidation. Use when your\n  agent generates static content (portfolios, news pages, link hubs,\n  single-purpose landing pages) and wants to share it as a public URL.\n  Free tier included, no credit card needed.\nversion: 1.0.1\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - CLAWSITE_API_KEY\n        - CLAWSITE_SITE_ID\n    primaryEnv: CLAWSITE_API_KEY\n    homepage: https://clawsite.ai\n---\n\n# Clawsite.ai — Static Website Hosting for AI Agents\n\n## When to Use Clawsite\n\n- Your agent generates static HTML / CSS / JS / images and wants to publish them as a public URL\n- You want a zero-config hosting account with HTTPS, CloudFront CDN, and atomic deploys\n- You want a memorable random URL like `happy-otter-42.clawsite.ai` (the slug is auto-generated; you can't pick it)\n\n## Quick Start\n\n**Your sandbox already has an account provisioned.** Check these env vars before doing anything else:\n\n| Env var | Required? | Purpose |\n|---|---|---|\n| `CLAWSITE_API_KEY` | **required** | Bearer token for all authenticated endpoints (e.g. `csk_live_...`) |\n| `CLAWSITE_SITE_ID` | **required** | Your assigned site identifier (e.g. `site_01KQ...`) |\n| `CLAWSITE_URL` | informational | Your live site URL, the one to share with the user (e.g. `https://happy-otter-42.clawsite.ai`). If unset, derive from `GET /v1/sites`. |\n| `CLAWSITE_API_URL` | optional | API base URL. **Defaults to `https://api.clawsite.ai` if unset.** Dev sandboxes override to `https://api.dev.clawsite.ai`. |\n\nIf `CLAWSITE_API_KEY` or `CLAWSITE_SITE_ID` is unset, see \"Standalone Registration\" at the bottom.\n\n> Examples below use `$CLAWSITE_API_URL` literally; if it's unset, fall back to `https://api.clawsite.ai`.\n\n**API base: `$CLAWSITE_API_URL`/v1**\n\nAll authenticated endpoints require `Authorization: Bearer $CLAWSITE_API_KEY`.\n\n### 1. Deploy a directory of static files\n\nThe deploy endpoint takes a `.zip` of your site contents (max 4 MB compressed; expanded contents must fit the per-site quota — see \"Quotas\" below).\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\nAuthorization: Bearer $CLAWSITE_API_KEY\nContent-Type: application/zip\n\n(body: raw bytes of the .zip)\n\nWorkflow:\n\n1. Create your files in a directory:\n   ```\n   site/\n     index.html\n     style.css\n     app.js\n     images/logo.png\n   ```\n2. Zip the **contents** of the directory (no parent directory inside the zip):\n   ```\n   cd site/ && zip -r ../site.zip .\n   ```\n3. POST the zip:\n   ```\n   curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\"\n   ```\n\n-> Returns: `siteId`, `url`, `fileCount`, `sizeBytes`, `deployedAt` (Unix seconds)\n\n```json\n{\n  \"siteId\": \"site_...\",\n  \"url\": \"https://happy-otter-42.clawsite.ai\",\n  \"fileCount\": 12,\n  \"sizeBytes\": 458231,\n  \"deployedAt\": 1744732800\n}\n```\n\n**Atomic full-site replacement:** any files from the previous deploy that are NOT in the new zip get deleted. Deploy = full snapshot, not incremental upload.\n\n**Cache:** every deploy automatically invalidates CloudFront cache. Manual purge below is rarely needed.\n\n**Routing:** the path inside the zip becomes the URL path. `index.html` at the zip root is served at `/`. Subdirectories work: `images/logo.png` is at `/images/logo.png`. For pretty URLs without `.html`, name files like `about/index.html` and link as `/about/`.\n\n### 2. Show the user their site\n\nThe site is live at `$CLAWSITE_URL` immediately after a successful deploy. Tell the user:\n\n> \"Your site is live at $CLAWSITE_URL\"\n\n### 3. Purge CloudFront cache (rarely needed)\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/purge-cache\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n(no body)\n\n-> Returns: `siteId`, `purgedAt`\n\n```json\n{ \"siteId\": \"site_...\", \"purgedAt\": 1744732800 }\n```\n\nUse this only if the cache is serving stale content unrelated to a deploy. Normal deploys auto-invalidate.\n\n### 4. List sites and check quota usage\n\nGET $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n-> Returns: array of `{ siteId, slug, url, sizeBytes, fileCount, lastDeployAt }`\n\n```json\n{\n  \"sites\": [{\n    \"siteId\": \"site_...\",\n    \"slug\": \"happy-otter-42\",\n    \"url\": \"https://happy-otter-42.clawsite.ai\",\n    \"sizeBytes\": 458231,\n    \"fileCount\": 12,\n    \"lastDeployAt\": 1744732800\n  }]\n}\n```\n\nUse this to verify your `CLAWSITE_SITE_ID` matches and to inspect current usage vs quotas.\n\n## Other Endpoints\n\n### Delete a site\n\nDELETE $CLAWSITE_API_URL/v1/sites/{siteId}\nAuthorization: Bearer $CLAWSITE_API_KEY\n\nPermanently deletes the site and every file under it. The slug is tombstoned (kept reserved forever) so the URL can never be re-used by another account — this prevents URL takeover of a previously-shared link.\n\n## Standalone Registration (no sandbox env vars)\n\nIf you're running outside a ZenClaw sandbox and `CLAWSITE_API_KEY` isn't pre-set, register via email OTP. Verification is delegated to MBID (MixerBox ID).\n\n**Step 1** — request a 6-digit code via email:\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"email\": \"your-email@example.com\" }\n```\n\n-> Returns: `{ \"challengeId\": \"<JWT>\" }`\n\n**Step 2** — verify (after the 6-digit code arrives in your inbox):\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"challengeId\": \"<JWT from step 1>\", \"code\": \"123456\" }\n```\n\n-> Returns: `accountId`, `apiKey`, and a default `sites[]` entry. **Save the `apiKey` immediately — it cannot be recovered.**\n\nYou can also create additional sites later via:\n\nPOST $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $apiKey\n\n(no body needed; slug is auto-assigned)\n\n> Note: in v1 the per-account site quota is 1, so this returns 409 `quota_exceeded` if you already have one site.\n\n## Quotas (v1)\n\n| Item | Limit |\n|---|---|\n| Sites per account | **1** |\n| Storage per site | **3 MB** (uncompressed total) |\n| Max single file | **1 MB** |\n| Max files per site | **100** |\n| Max compressed zip body | **4 MB** |\n| Deploy frequency | **10 / hour** per account |\n| Purge frequency | **5 / hour** per account |\n| Bandwidth | **unlimited** |\n\n**Allowed file extensions:** `html`, `css`, `js`, `json`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico`, `woff2`, `txt`, `md`.\n\nAnything else (e.g. `.php`, `.exe`, `.py`) → 400 `unsupported_extension`.\n\n## Errors\n\nAll errors return JSON:\n\n```json\n{ \"error\": { \"code\": \"<machine-code>\", \"message\": \"<human-readable>\" } }\n```\n\n| Code | HTTP | Cause |\n|---|---|---|\n| `unauthorized` | 401 | Missing or invalid API key |\n| `missing_fields` | 400 | Required fields absent or malformed (e.g. invalid email shape on register) |\n| `not_found` | 404 | Site doesn't belong to your account, or doesn't exist |\n| `quota_exceeded` | 409 | Sites limit, storage limit, or rate limit hit |\n| `unsupported_extension` | 400 | File extension not in the whitelist above |\n| `file_too_large` | 400 | Single file > 1 MB, or zip body > 4 MB |\n| `invalid_zip` | 400 | Body not a valid zip, missing body, or contains paths with `..` / absolute paths |\n| `mbid_error` | upstream | Forwarded from MBID's email-verify endpoints (`mx_record_not_found`, `domain_typo`, `too_many_request`, `incorrect_verification_code`) — only relevant during email registration |\n\n**Idempotency note:** re-calling `/v1/register` with the same MBID-verified email returns the existing `accountId`, `apiKey`, and `sites`. The same `apiKey` is returned every time so a partner orchestrator (e.g. ZenClaw) can re-provision sandboxes for the same user without losing access. If you ever want a fresh key, `DELETE` the site and re-register.\n\nFile v1.0.1:README.md\n\n# ClawSite\n\nAgent-first static website hosting microservice. Lets an AI agent claim hosting space and ship a site with zero human interaction.\n\n- **Production domain:** `clawsite.ai`\n- **Development domain:** `dev.clawsite.ai`\n- **First partner:** ZenClaw (`MixerBox/zenclaw`)\n- **Sibling service:** [`MixerBox/clawmail`](https://github.com/MixerBox/clawmail) — same operational shape (Terraform, esbuild, multi-Lambda, ClawHub-published skill)\n\nSee `CLAUDE.md` for quick architecture reference and `docs/superpowers/specs/` for the v1 design spec.\n\n## Status\n\n🚧 v1 implementation in progress. Repo bootstrapped 2026-04-17.\n\nFile v1.0.1:_meta.json\n\n{\n  \"ownerId\": \"kn7e574f4e1ewbqp1xvm49jqss862wx3\",\n  \"slug\": \"clawsite-ai\",\n  \"version\": \"1.0.1\",\n  \"publishedAt\": 1777888741627\n}\n\nFile v1.0.1:CLAUDE.md\n\n# ClawSite\n\nAgent-first static website hosting microservice, modeled after [ClawMail](https://github.com/MixerBox/clawmail).\n\n## Status (2026-04-29)\n\n✅ **v1 deployed to dev + prod, smoke-tested live, partner mode confirmed working end-to-end.**\n\n| Env | API | Site URL pattern | Partner secret on |\n|---|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` | `claws-instance-controller-staging` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` | `claws-instance-controller` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n\nCloudFront distribution IDs: dev `E3VZOWBAWTVYBK`, prod `ESXYWGNPX39MP`.\n\nBoth envs live in AWS account `974718210214` (us-east-1), same account as ClawMail. Route53 zone `clawsite.ai` is shared between dev/prod.\n\n**Email-OTP path is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps in MBID `TABLE_APP`.** Once that's done, set `MBID_SERVER_KEY` on the api Lambda env (Terraform var: `mbid_server_key`) and email registration unlocks. Partner mode (the only path ZenClaw uses today) doesn't depend on this.\n\n## Reference repos\n\n- `~/Work/MixerBox/clawmail/` — the structural model. When in doubt about a pattern (`globalThis.__routes`, Vitest mocks, Terraform), look at how ClawMail does it.\n- `~/Work/MixerBox/zenclaw/` — the first partner. Plan 2 (`configure-clawsite` controller action + sandbox env injection + `clawsite-ai` skill install) lands there.\n- `~/Work/MixerBox/microservice/microservice/kubernetes/livapp/apps/mb-id/` — MBID service we delegate email verification to.\n\n## Quick reference\n\n- **Language:** TypeScript (Node.js 20, ES2022 modules)\n- **Test:** `npx vitest run`\n- **Typecheck:** `npx tsc --noEmit`\n- **Build:** `node build.mjs` (outputs `.mjs` to `dist/`)\n- **Deploy** (Terraform: `terraform/` dir, two workspaces `dev` + `prod`):\n  ```bash\n  # build + zip Lambda artifact (load-bearing filename: api.zip)\n  node build.mjs\n  cd dist && zip -j api.zip api.mjs api.mjs.map\n\n  # dev — uses claws-instance-controller-staging partner secret\n  cd ../terraform && terraform workspace select dev && terraform apply \\\n    -var \"site_domain=dev.clawsite.ai\" \\\n    -var \"api_domain=api.dev.clawsite.ai\" \\\n    -var \"environment=staging\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller-staging \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n\n  # prod — uses claws-instance-controller partner secret\n  terraform workspace select prod && terraform apply \\\n    -var \"site_domain=clawsite.ai\" \\\n    -var \"api_domain=api.clawsite.ai\" \\\n    -var \"environment=prod\" \\\n    -var \"mbid_api_base_url=https://api.id.mixerbox.com\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n  ```\n\n  The `partner_secret` flow: ZenClaw's controller Lambda holds `PARTNER_CLAWSITE_SECRET` as an env var (set once via `aws lambda update-function-configuration`); ClawSite's Terraform reads it at apply time so the two sides cannot drift. Same pattern as ClawMail's `PARTNER_CLAWMAIL_SECRET`.\n- **Hot-patch Lambda code only** (when only src/ changed, no infra): `node build.mjs && cd dist && zip -j api.zip api.mjs api.mjs.map && for fn in clawsite-staging-api clawsite-api; do aws lambda update-function-code --function-name \"$fn\" --zip-file fileb://api.zip; done`\n- **Publish skill to ClawHub:** `clawhub publish .` (run from repo root; `SKILL.md` is the manifest)\n- **Spec:** [`docs/superpowers/specs/2026-04-15-clawsite-design.md`](docs/superpowers/specs/2026-04-15-clawsite-design.md)\n\n### Resource naming convention\n\n`local.prefix` in `terraform/main.tf` derives the AWS resource name prefix from `var.environment`:\n\n- `var.environment = \"prod\"` → `local.prefix = \"clawsite\"` → `clawsite-api`, `clawsite-manifest`, `clawsite-sites-<account>`\n- anything else (typically `staging` for dev workspace) → `local.prefix = \"clawsite-${env}\"` → `clawsite-staging-api`, `clawsite-staging-manifest`, `clawsite-staging-sites-<account>`\n\nThe dev workspace uses `environment=staging` to match ZenClaw's `claws-instance-controller-staging` controller naming. **Always pass `-var environment=staging` for dev applies** (the variable's default is `dev` for legacy reasons but should not be relied on).\n\n## Architecture (single Lambda)\n\n| Lambda | Trigger | Purpose |\n|---|---|---|\n| `api` | API Gateway HTTP API (`ANY /{proxy+}`) | All REST endpoints — register, sites CRUD, deploy, purge-cache |\n\nThere used to be a `cleanup` Lambda intended for daily housekeeping, but it had no EventBridge trigger and nothing to clean (quota counters self-expire via DDB TTL; slug tombstones are by-design permanent). Removed 2026-04-30. If real housekeeping logic emerges later, re-add the Lambda + EventBridge rule together.\n\nStorage:\n- **DynamoDB** single table `clawsite[-staging]-manifest` — accounts, API keys, sites, slug reservations, quota counters. GSIs: `byApiKeyHash`, `byMbid`.\n- **S3** `clawsite[-staging]-sites-<account-id>` — static site files, partitioned by slug prefix.\n- **CloudFront** distribution + CloudFront Function for `<slug>` host → S3 prefix routing.\n\n## MBID integration (email OTP)\n\nEmail OTP registration delegates to **MBID** (`api.id.{dev.,}mixerbox.com`). ClawSite calls:\n- `POST /api/request_email_verify` → returns a `verifyToken` JWT we pass back as our `challengeId`\n- `PUT /api/verify_email` → returns `user.uuid` which becomes our `mbidUserId`\n\nNet effect: **every ClawSite account is keyed by an MBID UUID**. Partner-mode registration (ZenClaw) and email-OTP registration both deduplicate via the same `byMbid` GSI — one user, one ClawSite account, regardless of how they arrived.\n\nTo activate email-OTP path:\n1. Register `clawsite-dev` and `clawsite-prod` as apps in MBID's `TABLE_APP` to obtain `serverKey` values\n2. Update Lambda env vars: `MBID_SERVER_KEY` (the serverKey for that env) and `MBID_API_BASE_URL` (already set per env)\n\nEmail-OTP code is in place with mocked tests; real serverKey just unlocks the production path.\n\n### Removed from original spec (no longer relevant)\n\nThe spec originally called for self-hosted SES + SQS + a `deliver-email` Lambda. The MBID rework dropped all of that:\n\n- ❌ No SES domain identity / DKIM / SPF / DMARC\n- ❌ No SQS verification queue / DLQ\n- ❌ No `deliver-email` Lambda (architecture went 3 → 2 Lambdas)\n- ❌ No DynamoDB `challenge#*` items (MBID's own `verifyToken` JWT is stateless on our side)\n\nIf re-reading the spec, treat any reference to those items as historical.\n\n## Branching\n\n- `main` — what's deployed to prod (auto-deploy is manual via `terraform apply` on prod workspace)\n- `develop` — staging-equivalent; merges back to `main` after smoke tests\n- Bug fixes / features land via `fix/*` or `feat/*` branches → develop → main\n\n## Project structure\n\n```\nsrc/\n├── handlers/\n│   ├── api.ts              # API Gateway entry; regex route table on globalThis.__routes\n│   └── routes/             # one file per endpoint group, registered via side-effect imports\n│       ├── register.ts\n│       ├── sites.ts\n│       ├── deploy.ts\n│       └── purge.ts\n├── lib/                    # AWS-thin utilities\n│   ├── auth.ts             # SHA-256 + timing-safe apiKey verify + partner secret check\n│   ├── cloudfront.ts       # invalidatePrefix() helper\n│   ├── dynamo.ts           # DDB DocumentClient + PK/SK builders + table/GSI names + hourWindow()\n│   ├── errors.ts           # AppError class, JSON response helpers, CORS headers\n│   ├── id.ts               # acc_, site_, csk_live_* ID generators\n│   ├── mbid.ts             # MBID thin client (requestEmailVerify, verifyEmail)\n│   ├── quota.ts            # Hourly window counters with atomic increments\n│   ├── s3.ts               # S3 client + bucket name\n│   ├── slug.ts             # Random adjective-animal-NN slug + fallback hex slug\n│   ├── validate.ts         # File extension whitelist, size limits, QUOTA constants\n│   └── zip.ts              # Streaming zip extraction with validation\n└── middleware/\n    └── auth.ts             # Bearer apiKey → AuthContext via byApiKeyHash GSI\nbuild.mjs                   # esbuild bundler (entry: api.ts, cleanup.ts)\nterraform/                  # AWS infra; workspaces dev + prod\ntests/                      # Vitest tree mirrors src/\ndocs/superpowers/{specs,plans}/  # Design specs and execution plans\n```\n\n## Key patterns\n\n### Route registration\nRoutes register via side-effect imports in `src/handlers/api.ts`. Each route file calls `route(method, path, handler)` at module level. Routes are stored in `globalThis.__routes` to avoid ESM circular-import TDZ issues — same pattern as ClawMail.\n\nPublic (no-auth) routes: `route('POST', '/path', handler, { public: true })` — currently only `/v1/register`.\n\nThe `route()` function defensively initializes `globalThis.__routes` because esbuild bundling can place dependency module top-level code before the entry's initialization. See comment in `api.ts` for context.\n\n### Auth\n`Authorization: Bearer <token>`:\n- `csk_live_*` → SHA-256 hash → DynamoDB GSI lookup `byApiKeyHash` (per-account API key)\n- Partner secret → on `POST /v1/register` only, matched in handler before falling through to email-mode body shapes\n\n### Single-table DynamoDB\nActive PK/SK prefixes (live in code):\n- `account#<id>` + `meta` / `apikey#<hash>` / `site#<id>` / `quota#<action>#<window>`\n- `slug#<slug>` + `meta` (global slug reservation; status flips to `tombstoned` on site delete and stays forever — prevents URL takeover)\n\n`account#<id>` + `meta` includes a `sitesCount` numeric field, atomically updated by site CRUD operations to enforce the per-account site quota race-free (see `src/handlers/routes/sites.ts`).\n\nGSIs: `byApiKeyHash` (apiKey lookup → accountId), `byMbid` (MBID UUID → accountId for cross-flow idempotency).\n\n### Quota enforcement\n- Per-account site count: atomic via `sitesCount` counter on account meta + conditional update inside the site-creation TransactWrite. Can never race.\n- Per-hour deploy / purge frequency: hourly window counter in `src/lib/quota.ts`, gated on a single conditional `ADD count :one` inside the action handler.\n\n### ClawHub publishing\n`SKILL.md` at the repo root is the agent-facing API doc + ClawHub manifest. Published via `clawhub publish .`. Skill slug: `clawsite-ai`. Once Plan 2 lands, ZenClaw sandboxes will `clawhub install clawsite-ai` during provisioning.\n\n### Testing\nVitest with `vi.mock('@lib/dynamo')`. Route handler tests also mock `@handlers/api` to export a no-op `route` function, avoiding circular imports. Path aliases live in both `tsconfig.json` and `vitest.config.ts` — keep them in sync.\n\n## Known gaps / follow-ups\n\n- **Plan 2 (ZenClaw integration)** is in flight in `~/Work/MixerBox/zenclaw` on `feat/configure-clawsite-action`. Adds the `configure-clawsite` controller action + manifest fields. Sandbox-side env injection + `clawhub install clawsite-ai` are still TODO.\n\nFile v1.0.1:package.json\n\n{\n  \"name\": \"clawsite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Agent-first static website hosting microservice (clawsite.ai)\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"node build.mjs\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-cloudfront\": \"^3.1007.0\",\n    \"@aws-sdk/client-dynamodb\": \"^3.1007.0\",\n    \"@aws-sdk/client-s3\": \"^3.1007.0\",\n    \"@aws-sdk/lib-dynamodb\": \"^3.1007.0\",\n    \"ulid\": \"^3.0.2\",\n    \"yauzl\": \"^3.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/aws-lambda\": \"^8.10.161\",\n    \"@types/node\": \"^25.4.0\",\n    \"@types/yauzl\": \"^2.10.3\",\n    \"@types/yazl\": \"^3.3.1\",\n    \"esbuild\": \"^0.27.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"yazl\": \"^3.3.1\"\n  }\n}\n\nArchive v1.0.0: 5 files, 9798 bytes\n\nFiles: CLAUDE.md (11365b), package.json (784b), README.md (629b), SKILL.md (7816b), _meta.json (130b)\n\nFile v1.0.0:SKILL.md\n\n---\nname: clawsite-ai\ndescription: >-\n  Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai\n  URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call,\n  atomic full-site replace, automatic CDN cache invalidation. Use when your\n  agent generates static content (portfolios, news pages, link hubs,\n  single-purpose landing pages) and wants to share it as a public URL.\n  Free tier included, no credit card needed.\nversion: 1.0.0\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - CLAWSITE_API_KEY\n        - CLAWSITE_SITE_ID\n    primaryEnv: CLAWSITE_API_KEY\n    homepage: https://clawsite.ai\n---\n\n# Clawsite.ai — Static Website Hosting for AI Agents\n\n## When to Use Clawsite\n\n- Your agent generates static HTML / CSS / JS / images and wants to publish them as a public URL\n- You want a zero-config hosting account with HTTPS, CloudFront CDN, and atomic deploys\n- You want a memorable random URL like `happy-otter-42.clawsite.ai` (the slug is auto-generated; you can't pick it)\n\n## Quick Start\n\n**Your sandbox already has an account provisioned.** Check these env vars before doing anything else:\n\n| Env var | Required? | Purpose |\n|---|---|---|\n| `CLAWSITE_API_KEY` | **required** | Bearer token for all authenticated endpoints (e.g. `csk_live_...`) |\n| `CLAWSITE_SITE_ID` | **required** | Your assigned site identifier (e.g. `site_01KQ...`) |\n| `CLAWSITE_URL` | informational | Your live site URL, the one to share with the user (e.g. `https://happy-otter-42.clawsite.ai`). If unset, derive from `GET /v1/sites`. |\n| `CLAWSITE_API_URL` | optional | API base URL. **Defaults to `https://api.clawsite.ai` if unset.** Dev sandboxes override to `https://api.dev.clawsite.ai`. |\n\nIf `CLAWSITE_API_KEY` or `CLAWSITE_SITE_ID` is unset, see \"Standalone Registration\" at the bottom.\n\n> Examples below use `$CLAWSITE_API_URL` literally; if it's unset, fall back to `https://api.clawsite.ai`.\n\n**API base: `$CLAWSITE_API_URL`/v1**\n\nAll authenticated endpoints require `Authorization: Bearer $CLAWSITE_API_KEY`.\n\n### 1. Deploy a directory of static files\n\nThe deploy endpoint takes a `.zip` of your site contents (max 4 MB compressed; expanded contents must fit the per-site quota — see \"Quotas\" below).\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\nAuthorization: Bearer $CLAWSITE_API_KEY\nContent-Type: application/zip\n\n(body: raw bytes of the .zip)\n\nWorkflow:\n\n1. Create your files in a directory:\n   ```\n   site/\n     index.html\n     style.css\n     app.js\n     images/logo.png\n   ```\n2. Zip the **contents** of the directory (no parent directory inside the zip):\n   ```\n   cd site/ && zip -r ../site.zip .\n   ```\n3. POST the zip:\n   ```\n   curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\"\n   ```\n\n-> Returns: `siteId`, `url`, `fileCount`, `sizeBytes`, `deployedAt` (Unix seconds)\n\n```json\n{\n  \"siteId\": \"site_...\",\n  \"url\": \"https://happy-otter-42.clawsite.ai\",\n  \"fileCount\": 12,\n  \"sizeBytes\": 458231,\n  \"deployedAt\": 1744732800\n}\n```\n\n**Atomic full-site replacement:** any files from the previous deploy that are NOT in the new zip get deleted. Deploy = full snapshot, not incremental upload.\n\n**Cache:** every deploy automatically invalidates CloudFront cache. Manual purge below is rarely needed.\n\n**Routing:** the path inside the zip becomes the URL path. `index.html` at the zip root is served at `/`. Subdirectories work: `images/logo.png` is at `/images/logo.png`. For pretty URLs without `.html`, name files like `about/index.html` and link as `/about/`.\n\n### 2. Show the user their site\n\nThe site is live at `$CLAWSITE_URL` immediately after a successful deploy. Tell the user:\n\n> \"Your site is live at $CLAWSITE_URL\"\n\n### 3. Purge CloudFront cache (rarely needed)\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/purge-cache\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n(no body)\n\n-> Returns: `siteId`, `purgedAt`\n\n```json\n{ \"siteId\": \"site_...\", \"purgedAt\": 1744732800 }\n```\n\nUse this only if the cache is serving stale content unrelated to a deploy. Normal deploys auto-invalidate.\n\n### 4. List sites and check quota usage\n\nGET $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $CLAWSITE_API_KEY\n\n-> Returns: array of `{ siteId, slug, url, sizeBytes, fileCount, lastDeployAt }`\n\n```json\n{\n  \"sites\": [{\n    \"siteId\": \"site_...\",\n    \"slug\": \"happy-otter-42\",\n    \"url\": \"https://happy-otter-42.clawsite.ai\",\n    \"sizeBytes\": 458231,\n    \"fileCount\": 12,\n    \"lastDeployAt\": 1744732800\n  }]\n}\n```\n\nUse this to verify your `CLAWSITE_SITE_ID` matches and to inspect current usage vs quotas.\n\n## Other Endpoints\n\n### Delete a site\n\nDELETE $CLAWSITE_API_URL/v1/sites/{siteId}\nAuthorization: Bearer $CLAWSITE_API_KEY\n\nPermanently deletes the site and every file under it. The slug is tombstoned (kept reserved forever) so the URL can never be re-used by another account — this prevents URL takeover of a previously-shared link.\n\n## Standalone Registration (no sandbox env vars)\n\nIf you're running outside a ZenClaw sandbox and `CLAWSITE_API_KEY` isn't pre-set, register via email OTP. Verification is delegated to MBID (MixerBox ID).\n\n**Step 1** — request a 6-digit code via email:\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"email\": \"your-email@example.com\" }\n```\n\n-> Returns: `{ \"challengeId\": \"<JWT>\" }`\n\n**Step 2** — verify (after the 6-digit code arrives in your inbox):\n\nPOST $CLAWSITE_API_URL/v1/register\nContent-Type: application/json\n\n```json\n{ \"challengeId\": \"<JWT from step 1>\", \"code\": \"123456\" }\n```\n\n-> Returns: `accountId`, `apiKey`, and a default `sites[]` entry. **Save the `apiKey` immediately — it cannot be recovered.**\n\nYou can also create additional sites later via:\n\nPOST $CLAWSITE_API_URL/v1/sites\nAuthorization: Bearer $apiKey\n\n(no body needed; slug is auto-assigned)\n\n> Note: in v1 the per-account site quota is 1, so this returns 409 `quota_exceeded` if you already have one site.\n\n## Quotas (v1)\n\n| Item | Limit |\n|---|---|\n| Sites per account | **1** |\n| Storage per site | **3 MB** (uncompressed total) |\n| Max single file | **1 MB** |\n| Max files per site | **100** |\n| Max compressed zip body | **4 MB** |\n| Deploy frequency | **10 / hour** per account |\n| Purge frequency | **5 / hour** per account |\n| Bandwidth | **unlimited** |\n\n**Allowed file extensions:** `html`, `css`, `js`, `json`, `svg`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico`, `woff2`, `txt`, `md`.\n\nAnything else (e.g. `.php`, `.exe`, `.py`) → 400 `unsupported_extension`.\n\n## Errors\n\nAll errors return JSON:\n\n```json\n{ \"error\": { \"code\": \"<machine-code>\", \"message\": \"<human-readable>\" } }\n```\n\n| Code | HTTP | Cause |\n|---|---|---|\n| `unauthorized` | 401 | Missing or invalid API key |\n| `missing_fields` | 400 | Required fields absent or malformed (e.g. invalid email shape on register) |\n| `not_found` | 404 | Site doesn't belong to your account, or doesn't exist |\n| `quota_exceeded` | 409 | Sites limit, storage limit, or rate limit hit |\n| `unsupported_extension` | 400 | File extension not in the whitelist above |\n| `file_too_large` | 400 | Single file > 1 MB, or zip body > 4 MB |\n| `invalid_zip` | 400 | Body not a valid zip, missing body, or contains paths with `..` / absolute paths |\n| `mbid_error` | upstream | Forwarded from MBID's email-verify endpoints (`mx_record_not_found`, `domain_typo`, `too_many_request`, `incorrect_verification_code`) — only relevant during email registration |\n\n**Idempotency note:** re-calling `/v1/register` with the same MBID-verified email returns the existing `accountId` + `sites` but **without `apiKey`** (only the hash is stored). If you lose your `apiKey`, the only recovery is `DELETE` the site and re-register fresh.\n\nFile v1.0.0:README.md\n\n# ClawSite\n\nAgent-first static website hosting microservice. Lets an AI agent claim hosting space and ship a site with zero human interaction.\n\n- **Production domain:** `clawsite.ai`\n- **Development domain:** `dev.clawsite.ai`\n- **First partner:** ZenClaw (`MixerBox/zenclaw`)\n- **Sibling service:** [`MixerBox/clawmail`](https://github.com/MixerBox/clawmail) — same operational shape (Terraform, esbuild, multi-Lambda, ClawHub-published skill)\n\nSee `CLAUDE.md` for quick architecture reference and `docs/superpowers/specs/` for the v1 design spec.\n\n## Status\n\n🚧 v1 implementation in progress. Repo bootstrapped 2026-04-17.\n\nFile v1.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn7e574f4e1ewbqp1xvm49jqss862wx3\",\n  \"slug\": \"clawsite-ai\",\n  \"version\": \"1.0.0\",\n  \"publishedAt\": 1777886755391\n}\n\nFile v1.0.0:CLAUDE.md\n\n# ClawSite\n\nAgent-first static website hosting microservice, modeled after [ClawMail](https://github.com/MixerBox/clawmail).\n\n## Status (2026-04-29)\n\n✅ **v1 deployed to dev + prod, smoke-tested live, partner mode confirmed working end-to-end.**\n\n| Env | API | Site URL pattern | Partner secret on |\n|---|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` | `claws-instance-controller-staging` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` | `claws-instance-controller` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n\nCloudFront distribution IDs: dev `E3VZOWBAWTVYBK`, prod `ESXYWGNPX39MP`.\n\nBoth envs live in AWS account `974718210214` (us-east-1), same account as ClawMail. Route53 zone `clawsite.ai` is shared between dev/prod.\n\n**Email-OTP path is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps in MBID `TABLE_APP`.** Once that's done, set `MBID_SERVER_KEY` on the api Lambda env (Terraform var: `mbid_server_key`) and email registration unlocks. Partner mode (the only path ZenClaw uses today) doesn't depend on this.\n\n## Reference repos\n\n- `~/Work/MixerBox/clawmail/` — the structural model. When in doubt about a pattern (`globalThis.__routes`, Vitest mocks, Terraform), look at how ClawMail does it.\n- `~/Work/MixerBox/zenclaw/` — the first partner. Plan 2 (`configure-clawsite` controller action + sandbox env injection + `clawsite-ai` skill install) lands there.\n- `~/Work/MixerBox/microservice/microservice/kubernetes/livapp/apps/mb-id/` — MBID service we delegate email verification to.\n\n## Quick reference\n\n- **Language:** TypeScript (Node.js 20, ES2022 modules)\n- **Test:** `npx vitest run`\n- **Typecheck:** `npx tsc --noEmit`\n- **Build:** `node build.mjs` (outputs `.mjs` to `dist/`)\n- **Deploy** (Terraform: `terraform/` dir, two workspaces `dev` + `prod`):\n  ```bash\n  # build + zip Lambda artifact (load-bearing filename: api.zip)\n  node build.mjs\n  cd dist && zip -j api.zip api.mjs api.mjs.map\n\n  # dev — uses claws-instance-controller-staging partner secret\n  cd ../terraform && terraform workspace select dev && terraform apply \\\n    -var \"site_domain=dev.clawsite.ai\" \\\n    -var \"api_domain=api.dev.clawsite.ai\" \\\n    -var \"environment=staging\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller-staging \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n\n  # prod — uses claws-instance-controller partner secret\n  terraform workspace select prod && terraform apply \\\n    -var \"site_domain=clawsite.ai\" \\\n    -var \"api_domain=api.clawsite.ai\" \\\n    -var \"environment=prod\" \\\n    -var \"mbid_api_base_url=https://api.id.mixerbox.com\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n  ```\n\n  The `partner_secret` flow: ZenClaw's controller Lambda holds `PARTNER_CLAWSITE_SECRET` as an env var (set once via `aws lambda update-function-configuration`); ClawSite's Terraform reads it at apply time so the two sides cannot drift. Same pattern as ClawMail's `PARTNER_CLAWMAIL_SECRET`.\n- **Hot-patch Lambda code only** (when only src/ changed, no infra): `node build.mjs && cd dist && zip -j api.zip api.mjs api.mjs.map && for fn in clawsite-staging-api clawsite-api; do aws lambda update-function-code --function-name \"$fn\" --zip-file fileb://api.zip; done`\n- **Publish skill to ClawHub:** `clawhub publish .` (run from repo root; `SKILL.md` is the manifest)\n- **Spec:** [`docs/superpowers/specs/2026-04-15-clawsite-design.md`](docs/superpowers/specs/2026-04-15-clawsite-design.md)\n\n### Resource naming convention\n\n`local.prefix` in `terraform/main.tf` derives the AWS resource name prefix from `var.environment`:\n\n- `var.environment = \"prod\"` → `local.prefix = \"clawsite\"` → `clawsite-api`, `clawsite-manifest`, `clawsite-sites-<account>`\n- anything else (typically `staging` for dev workspace) → `local.prefix = \"clawsite-${env}\"` → `clawsite-staging-api`, `clawsite-staging-manifest`, `clawsite-staging-sites-<account>`\n\nThe dev workspace uses `environment=staging` to match ZenClaw's `claws-instance-controller-staging` controller naming. **Always pass `-var environment=staging` for dev applies** (the variable's default is `dev` for legacy reasons but should not be relied on).\n\n## Architecture (single Lambda)\n\n| Lambda | Trigger | Purpose |\n|---|---|---|\n| `api` | API Gateway HTTP API (`ANY /{proxy+}`) | All REST endpoints — register, sites CRUD, deploy, purge-cache |\n\nThere used to be a `cleanup` Lambda intended for daily housekeeping, but it had no EventBridge trigger and nothing to clean (quota counters self-expire via DDB TTL; slug tombstones are by-design permanent). Removed 2026-04-30. If real housekeeping logic emerges later, re-add the Lambda + EventBridge rule together.\n\nStorage:\n- **DynamoDB** single table `clawsite[-staging]-manifest` — accounts, API keys, sites, slug reservations, quota counters. GSIs: `byApiKeyHash`, `byMbid`.\n- **S3** `clawsite[-staging]-sites-<account-id>` — static site files, partitioned by slug prefix.\n- **CloudFront** distribution + CloudFront Function for `<slug>` host → S3 prefix routing.\n\n## MBID integration (email OTP)\n\nEmail OTP registration delegates to **MBID** (`api.id.{dev.,}mixerbox.com`). ClawSite calls:\n- `POST /api/request_email_verify` → returns a `verifyToken` JWT we pass back as our `challengeId`\n- `PUT /api/verify_email` → returns `user.uuid` which becomes our `mbidUserId`\n\nNet effect: **every ClawSite account is keyed by an MBID UUID**. Partner-mode registration (ZenClaw) and email-OTP registration both deduplicate via the same `byMbid` GSI — one user, one ClawSite account, regardless of how they arrived.\n\nTo activate email-OTP path:\n1. Register `clawsite-dev` and `clawsite-prod` as apps in MBID's `TABLE_APP` to obtain `serverKey` values\n2. Update Lambda env vars: `MBID_SERVER_KEY` (the serverKey for that env) and `MBID_API_BASE_URL` (already set per env)\n\nEmail-OTP code is in place with mocked tests; real serverKey just unlocks the production path.\n\n### Removed from original spec (no longer relevant)\n\nThe spec originally called for self-hosted SES + SQS + a `deliver-email` Lambda. The MBID rework dropped all of that:\n\n- ❌ No SES domain identity / DKIM / SPF / DMARC\n- ❌ No SQS verification queue / DLQ\n- ❌ No `deliver-email` Lambda (architecture went 3 → 2 Lambdas)\n- ❌ No DynamoDB `challenge#*` items (MBID's own `verifyToken` JWT is stateless on our side)\n\nIf re-reading the spec, treat any reference to those items as historical.\n\n## Branching\n\n- `main` — what's deployed to prod (auto-deploy is manual via `terraform apply` on prod workspace)\n- `develop` — staging-equivalent; merges back to `main` after smoke tests\n- Bug fixes / features land via `fix/*` or `feat/*` branches → develop → main\n\n## Project structure\n\n```\nsrc/\n├── handlers/\n│   ├── api.ts              # API Gateway entry; regex route table on globalThis.__routes\n│   └── routes/             # one file per endpoint group, registered via side-effect imports\n│       ├── register.ts\n│       ├── sites.ts\n│       ├── deploy.ts\n│       └── purge.ts\n├── lib/                    # AWS-thin utilities\n│   ├── auth.ts             # SHA-256 + timing-safe apiKey verify + partner secret check\n│   ├── cloudfront.ts       # invalidatePrefix() helper\n│   ├── dynamo.ts           # DDB DocumentClient + PK/SK builders + table/GSI names + hourWindow()\n│   ├── errors.ts           # AppError class, JSON response helpers, CORS headers\n│   ├── id.ts               # acc_, site_, csk_live_* ID generators\n│   ├── mbid.ts             # MBID thin client (requestEmailVerify, verifyEmail)\n│   ├── quota.ts            # Hourly window counters with atomic increments\n│   ├── s3.ts               # S3 client + bucket name\n│   ├── slug.ts             # Random adjective-animal-NN slug + fallback hex slug\n│   ├── validate.ts         # File extension whitelist, size limits, QUOTA constants\n│   └── zip.ts              # Streaming zip extraction with validation\n└── middleware/\n    └── auth.ts             # Bearer apiKey → AuthContext via byApiKeyHash GSI\nbuild.mjs                   # esbuild bundler (entry: api.ts, cleanup.ts)\nterraform/                  # AWS infra; workspaces dev + prod\ntests/                      # Vitest tree mirrors src/\ndocs/superpowers/{specs,plans}/  # Design specs and execution plans\n```\n\n## Key patterns\n\n### Route registration\nRoutes register via side-effect imports in `src/handlers/api.ts`. Each route file calls `route(method, path, handler)` at module level. Routes are stored in `globalThis.__routes` to avoid ESM circular-import TDZ issues — same pattern as ClawMail.\n\nPublic (no-auth) routes: `route('POST', '/path', handler, { public: true })` — currently only `/v1/register`.\n\nThe `route()` function defensively initializes `globalThis.__routes` because esbuild bundling can place dependency module top-level code before the entry's initialization. See comment in `api.ts` for context.\n\n### Auth\n`Authorization: Bearer <token>`:\n- `csk_live_*` → SHA-256 hash → DynamoDB GSI lookup `byApiKeyHash` (per-account API key)\n- Partner secret → on `POST /v1/register` only, matched in handler before falling through to email-mode body shapes\n\n### Single-table DynamoDB\nActive PK/SK prefixes (live in code):\n- `account#<id>` + `meta` / `apikey#<hash>` / `site#<id>` / `quota#<action>#<window>`\n- `slug#<slug>` + `meta` (global slug reservation; status flips to `tombstoned` on site delete and stays forever — prevents URL takeover)\n\n`account#<id>` + `meta` includes a `sitesCount` numeric field, atomically updated by site CRUD operations to enforce the per-account site quota race-free (see `src/handlers/routes/sites.ts`).\n\nGSIs: `byApiKeyHash` (apiKey lookup → accountId), `byMbid` (MBID UUID → accountId for cross-flow idempotency).\n\n### Quota enforcement\n- Per-account site count: atomic via `sitesCount` counter on account meta + conditional update inside the site-creation TransactWrite. Can never race.\n- Per-hour deploy / purge frequency: hourly window counter in `src/lib/quota.ts`, gated on a single conditional `ADD count :one` inside the action handler.\n\n### ClawHub publishing\n`SKILL.md` at the repo root is the agent-facing API doc + ClawHub manifest. Published via `clawhub publish .`. Skill slug: `clawsite-ai`. Once Plan 2 lands, ZenClaw sandboxes will `clawhub install clawsite-ai` during provisioning.\n\n### Testing\nVitest with `vi.mock('@lib/dynamo')`. Route handler tests also mock `@handlers/api` to export a no-op `route` function, avoiding circular imports. Path aliases live in both `tsconfig.json` and `vitest.config.ts` — keep them in sync.\n\n## Known gaps / follow-ups\n\n- **Plan 2 (ZenClaw integration)** is in flight in `~/Work/MixerBox/zenclaw` on `feat/configure-clawsite-action`. Adds the `configure-clawsite` controller action + manifest fields. Sandbox-side env injection + `clawhub install clawsite-ai` are still TODO.\n\nFile v1.0.0:package.json\n\n{\n  \"name\": \"clawsite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Agent-first static website hosting microservice (clawsite.ai)\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"node build.mjs\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-cloudfront\": \"^3.1007.0\",\n    \"@aws-sdk/client-dynamodb\": \"^3.1007.0\",\n    \"@aws-sdk/client-s3\": \"^3.1007.0\",\n    \"@aws-sdk/lib-dynamodb\": \"^3.1007.0\",\n    \"ulid\": \"^3.0.2\",\n    \"yauzl\": \"^3.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/aws-lambda\": \"^8.10.161\",\n    \"@types/node\": \"^25.4.0\",\n    \"@types/yauzl\": \"^2.10.3\",\n    \"@types/yazl\": \"^3.3.1\",\n    \"esbuild\": \"^0.27.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"yazl\": \"^3.3.1\"\n  }\n}","readmeExcerpt":"Skill: Clawsite Owner: dannylai999 Summary: Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call, atomic ful... Tags: latest:1.0.5 Version history: v1.0.5 | 2026-05-11T03:11:47.918Z | auto Version 1.0.5 – Documentation Update - Expanded SKILL.md with detailed instructions on how to handle the 409 quota_exceeded error for /dep","codeSnippets":[],"executableExamples":[{"language":"text","snippet":"site/\n     index.html\n     style.css\n     app.js\n     images/logo.png"},{"language":"text","snippet":"cd site/ && zip -r ../site.zip ."},{"language":"text","snippet":"curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\""},{"language":"json","snippet":"{\n  \"siteId\": \"site_...\",\n  \"url\": \"https://happy-otter-42.clawsite.ai\",\n  \"fileCount\": 12,\n  \"sizeBytes\": 458231,\n  \"deployedAt\": 1744732800\n}"},{"language":"json","snippet":"{ \"siteId\": \"site_...\", \"purgedAt\": 1744732800 }"},{"language":"json","snippet":"{\n  \"sites\": [{\n    \"siteId\": \"site_...\",\n    \"slug\": \"happy-otter-42\",\n    \"url\": \"https://happy-otter-42.clawsite.ai\",\n    \"sizeBytes\": 458231,\n    \"fileCount\": 12,\n    \"lastDeployAt\": 1744732800\n  }]\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: clawsite-ai\ndescription: >-\n  Static website hosting for AI agents. Get a dedicated <slug>.clawsite.ai\n  URL with HTTPS, deploy a zip of HTML / CSS / JS / images in one API call,\n  atomic full-site replace, automatic CDN cache invalidation. Use when your\n  agent generates static content (portfolios, news pages, link hubs,\n  single-purpose landing pages) and wants to share it as a public URL.\n  Free tier included, no credit card needed.\nversion: 1.0.5\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - CLAWSITE_API_KEY\n        - CLAWSITE_SITE_ID\n    primaryEnv: CLAWSITE_API_KEY\n    homepage: https://clawsite.ai\n---\n\n# Clawsite.ai — Static Website Hosting for AI Agents\n\n## When to Use Clawsite\n\n- Your agent generates static HTML / CSS / JS / images and wants to publish them as a public URL\n- You want a zero-config hosting account with HTTPS, CloudFront CDN, and atomic deploys\n- You want a memorable random URL like `happy-otter-42.clawsite.ai` (the slug is auto-generated; you can't pick it)\n\n## Quick Start\n\n**Your sandbox already has an account provisioned.** Check these env vars before doing anything else:\n\n| Env var | Required? | Purpose |\n|---|---|---|\n| `CLAWSITE_API_KEY` | **required** | Bearer token for all authenticated endpoints (`csk_live_*` format) |\n| `CLAWSITE_SITE_ID` | **required** | Your assigned site identifier (`site_<ulid>` format) |\n| `CLAWSITE_URL` | informational | Your live site URL, the one to share with the user (e.g. `https://happy-otter-42.clawsite.ai`). If unset, derive from `GET /v1/sites`. |\n| `CLAWSITE_API_URL` | optional | API base URL. **Defaults to `https://api.clawsite.ai` if unset.** Dev sandboxes override to `https://api.dev.clawsite.ai`. |\n\nIf `CLAWSITE_API_KEY` or `CLAWSITE_SITE_ID` is unset, see \"Standalone Registration\" at the bottom.\n\n> Examples below use `$CLAWSITE_API_URL` literally; if it's unset, fall back to `https://api.clawsite.ai`.\n\n**API base: `$CLAWSITE_API_URL`/v1**\n\nAll authenticated endpoints require `Authorization: Bearer $CLAWSITE_API_KEY`.\n\n### 1. Deploy a directory of static files\n\nThe deploy endpoint takes a `.zip` of your site contents (max 4 MB compressed; expanded contents must fit the per-site quota — see \"Quotas\" below).\n\nPOST $CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\nAuthorization: Bearer $CLAWSITE_API_KEY\nContent-Type: application/zip\n\n(body: raw bytes of the .zip)\n\nWorkflow:\n\n1. Create your files in a directory:\n   ```\n   site/\n     index.html\n     style.css\n     app.js\n     images/logo.png\n   ```\n2. Zip the **contents** of the directory (no parent directory inside the zip):\n   ```\n   cd site/ && zip -r ../site.zip .\n   ```\n3. POST the zip:\n   ```\n   curl -X POST \"$CLAWSITE_API_URL/v1/sites/$CLAWSITE_SITE_ID/deploy\" \\\n     -H \"Authorization: Bearer $CLAWSITE_API_KEY\" \\\n     -H \"Content-Type: application/zip\" \\\n     --data-binary \"@site.zip\"\n   ```\n\n-> Returns: `siteId`, `url`, `fileCount`, `sizeBytes`, `deployedAt` (Unix seconds)\n\n```json\n{\n  \"siteId\": \"site_"},{"path":"README.md","content":"# ClawSite\n\nAgent-first static website hosting microservice. Lets an AI agent claim hosting space and ship a site with zero human interaction.\n\n- **Production domain:** `clawsite.ai` — API at `api.clawsite.ai`\n- **Development domain:** `dev.clawsite.ai` — API at `api.dev.clawsite.ai`\n- **First partner:** ZenClaw (`MixerBox/zenclaw`)\n- **Skill:** `clawsite-ai` (published to ClawHub)\n- **Sibling service:** [`MixerBox/clawmail`](https://github.com/MixerBox/clawmail) — same operational shape (Terraform, esbuild, single-Lambda + CloudFront-fronted S3, ClawHub-published skill)\n\nSee `CLAUDE.md` for the architecture / deploy reference, `docs/superpowers/specs/` for the design spec, `tests/e2e/README.md` for live-API smoke tests.\n\n## Status\n\n✅ Deployed live to dev + prod. Both AWS account `974718210214` (us-east-1).\n\n| Env | API | Site URL pattern |\n|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` |\n\nZenClaw partner-mode integration is the active code path. Email-OTP registration is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps."},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7e574f4e1ewbqp1xvm49jqss862wx3\",\n  \"slug\": \"clawsite-ai\",\n  \"version\": \"1.0.5\",\n  \"publishedAt\": 1778469107918\n}"},{"path":"CLAUDE.md","content":"# ClawSite\n\nAgent-first static website hosting microservice, modeled after [ClawMail](https://github.com/MixerBox/clawmail).\n\n## Status (2026-04-29)\n\n✅ **v1 deployed to dev + prod, smoke-tested live, partner mode confirmed working end-to-end.**\n\n| Env | API | Site URL pattern | Partner secret on |\n|---|---|---|---|\n| Dev / staging | `https://api.dev.clawsite.ai` | `<slug>.dev.clawsite.ai` | `claws-instance-controller-staging` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n| Prod | `https://api.clawsite.ai` | `<slug>.clawsite.ai` | `claws-instance-controller` Lambda env (`PARTNER_CLAWSITE_SECRET`) |\n\nCloudFront distribution IDs: dev `E3VZOWBAWTVYBK`, prod `ESXYWGNPX39MP`.\n\nBoth envs live in AWS account `974718210214` (us-east-1), same account as ClawMail. Route53 zone `clawsite.ai` is shared between dev/prod.\n\n**Email-OTP path is implemented but inert until MBID provisions a `serverKey` for the `clawsite-dev` / `clawsite-prod` apps in MBID `TABLE_APP`.** Once that's done, set `MBID_SERVER_KEY` on the api Lambda env (Terraform var: `mbid_server_key`) and email registration unlocks. Partner mode (the only path ZenClaw uses today) doesn't depend on this.\n\n## Reference repos\n\n- `~/Work/MixerBox/clawmail/` — the structural model. When in doubt about a pattern (`globalThis.__routes`, Vitest mocks, Terraform), look at how ClawMail does it.\n- `~/Work/MixerBox/zenclaw/` — the first partner. Plan 2 (`configure-clawsite` controller action + sandbox env injection + `clawsite-ai` skill install) lands there.\n- `~/Work/MixerBox/microservice/microservice/kubernetes/livapp/apps/mb-id/` — MBID service we delegate email verification to.\n\n## Quick reference\n\n- **Language:** TypeScript (Node.js 20, ES2022 modules)\n- **Test:** `npx vitest run`\n- **Typecheck:** `npx tsc --noEmit`\n- **Build:** `node build.mjs` (outputs `.mjs` to `dist/`)\n- **Deploy** (Terraform: `terraform/` dir, two workspaces `dev` + `prod`):\n  ```bash\n  # build + zip Lambda artifact (load-bearing filename: api.zip)\n  node build.mjs\n  cd dist && zip -j api.zip api.mjs api.mjs.map\n\n  # dev — uses claws-instance-controller-staging partner secret\n  cd ../terraform && terraform workspace select dev && terraform apply \\\n    -var \"site_domain=dev.clawsite.ai\" \\\n    -var \"api_domain=api.dev.clawsite.ai\" \\\n    -var \"environment=staging\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller-staging \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n\n  # prod — uses claws-instance-controller partner secret\n  terraform workspace select prod && terraform apply \\\n    -var \"site_domain=clawsite.ai\" \\\n    -var \"api_domain=api.clawsite.ai\" \\\n    -var \"environment=prod\" \\\n    -var \"mbid_api_base_url=https://api.id.mixerbox.com\" \\\n    -var \"partner_secret=$(aws lambda get-function-configuration \\\n      --function-name claws-instance-controller \\\n      --query 'Environment.Variables.PARTNER_CLAWSITE_SECRET' --output text)\"\n  ```\n\n  The "},{"path":"skill-card.md","content":"## Description:\n\nStatic website hosting for AI agents with HTTPS, zip-based static asset deploys, atomic full-site replacement, and automatic CDN cache invalidation.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[dannylai999](https://clawhub.ai/user/dannylai999)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and AI agents use this skill to publish generated static websites, portfolios, link hubs, and landing pages to a public ClawSite URL. It guides agents through authenticated deployment, quota checks, cache purging, and safe handling of destructive operations.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A misconfigured or attacker-controlled API endpoint could expose the ClawSite API key.\n\nMitigation: Use only documented ClawSite API hosts, confirm overrides before use, and avoid printing or embedding API keys in deployed static files.\n\nRisk: The skill can publish public content and includes a delete endpoint that permanently tombstones a site URL.\n\nMitigation: Require explicit user confirmation before publishing public content or deleting a site, and explain that deletion is irreversible.\n\nRisk: The artifact includes production Terraform and Lambda operation commands that could affect live infrastructure when cloud credentials are present.\n\nMitigation: Do not run backend infrastructure commands unless the user intends to operate the ClawSite service and the agent environment is authorized for that purpose.\n\nRisk: Deploy rate limits may return quota errors that could be mistaken for an account problem.\n\nMitigation: On deploy quota errors, tell the user to wait for the rate limit window instead of deleting the site or re-registering.\n\n## Reference(s):\n\n- [ClawSite homepage](https://clawsite.ai)\n- [ClawHub skill page](https://clawhub.ai/dannylai999/skills/clawsite-ai)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown with inline shell commands and API request examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Guidance may include public deployment URLs, quota status, and confirmation prompts for destructive actions.]\n\n## Skill Version(s):\n\n1.0.5 (source: frontmatter and server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1544,"uniquenessScore":44,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T07:12:28.743Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T07:12:28.743Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T10:52:27.846Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}