# artifact-bin

Upload a self-contained HTML artifact, get back a **public-but-unguessable link** to share with your user.

## Auth — get a token (three ways, in order of preference)

Every `/api/artifacts` call needs a bearer token:

```
Authorization: Bearer mx_...
```

1. **Saved config** — check `~/.config/artifact-bin/config.json` for
   `{ "url", "token" }` (written by `artifactbin login`). If it exists, use it:
   publishes are attached to your user's account automatically.
2. **Your user gave you one** — use it.
3. **No token? Mint an anonymous one** — zero setup:

```
POST https://artifacts.minusxapi.com/api/tokens/anonymous
→ 201 { "id", "token": "mx_..." }
```

Save it for the session and reuse it for every call. Anonymous artifacts work
fully but belong to nobody — **tell your user**: *"to keep these under your
account, log in at https://artifacts.minusxapi.com and claim token `mx_...`"* (they paste it in the
Claim box on the dashboard). Claiming attaches everything the token already
published, past and future.

A `401` means the token is wrong or revoked — mint a fresh anonymous one or
ask your user; do not retry the same token.

## Endpoints

### Create an artifact

```
POST https://artifacts.minusxapi.com/api/artifacts
{ "title": "Optional title", "description": "Optional", "html": "<!doctype html>..." }
→ 201 { "id": "art_...", "slug": "...", "url": "https://artifacts.minusxapi.com/a/<slug>", "version": 1 }
```

Give `url` to your user — that's the deliverable. Example:

```bash
jq -n --rawfile html artifact.html '{"title":"My page","html":$html}' \
  | curl -sS -X POST https://artifacts.minusxapi.com/api/artifacts \
      -H "Authorization: Bearer $ARTIFACT_TOKEN" -H 'Content-Type: application/json' \
      --data-binary @-
```

### Update an artifact (the link never changes)

```
PUT https://artifacts.minusxapi.com/api/artifacts/<id>
{ "html": "<!doctype html>...", "title": "optional new title" }
→ 200 { "id", "slug", "url", "version": <bumped> }
```

Full replacement — send the complete new content, not a diff. The previous
version is archived server-side, so a bad edit is recoverable. Omitted
`title`/`description` keep their current values. Optionally include
`expectedVersion` (from your last read): a concurrent edit then answers
`409 {"error":"version_conflict","currentVersion":N}` instead of being
overwritten — re-read, merge, and retry with `expectedVersion: N`.

### Read one back (before editing)

```
GET https://artifacts.minusxapi.com/api/artifacts/<id>
→ 200 { "id", "slug", "url", "title", "description", "format", "html", "version", ... }
```

### List your artifacts

```
GET https://artifacts.minusxapi.com/api/artifacts
→ 200 { "artifacts": [ { "id", "slug", "url", "title", "version", "updated_at", ... } ] }
```

### Content tiers — pick ONE content field per request

1. **`markup`** — THE document tier: static JSX (treated as data — validated
   and interpreted, never executed) rendered LIVE with themes, layouts, and
   real charts. Use it for anything worth designing:

```
POST https://artifacts.minusxapi.com/api/artifacts
{ "markup": "<div data-design=\"tw\" className=\"@container p-8\"><h1 className=\"text-4xl font-bold\">Q3</h1>...</div>",
  "theme": "nocturne", "template": "editorial", "colorMode": "light" }
```

   - Vocabulary: plain HTML tags + ~60 kit components (Card, Tabs, Badge,
     SlideDeck/Slide, Grid/GridItem, …) + live data embeds (`<Question>`
     charts/tables, `<Number>`, `<Param>` filters) — **read
     https://artifacts.minusxapi.com/api/markup for the full reference before authoring.**
   - Style with Tailwind classes via `className` (no `<style>`, no `style=`).
   - `theme`: `modernist | classical | nocturne | organic | broadsheet | industry`;
     `template`: `editorial | deck | scrolly | dashboard`; `colorMode`: `light | dark`.
   - Reference your data artifacts as `ref:<artifactId>` (see data tiers).
   - Humans edit the SAME document WYSIWYG at `https://artifacts.minusxapi.com/e/<id>` — you and your
     user are editing one artifact, versioned together.
   - `jsx` is accepted as a deprecated alias of `markup`.

2. **`markdown`** — input convenience: converted to `markup` (story JSX) at
   the door and stored as a markup artifact. Quick docs with zero design
   effort; title defaults from the first `#` heading; same `theme` field.
   Read-back returns the JSX, not your markdown.

3. **`html`** — raw escape hatch, full control, you write everything.

4. **Data tiers** — `dataset` (a JSON array of flat rows, + optional
   `columns` type declarations), `viz` (a reusable chart recipe with
   `{{slot}}` bindings), `image` (a base64 `data:` URL). Create these first,
   then bind them in markup as `ref:<artifactId>`; dataset creation echoes
   the inferred columns so you know what to bind.

`PUT` accepts any tier and may switch an artifact between them; `GET` returns
the `markup` source plus `theme` for round-trip editing.

### MCP server (same API, tool-shaped)

`https://artifacts.minusxapi.com/mcp` — a Streamable HTTP MCP server speaking this exact API
(`create_artifact`, `update_artifact`, `get_artifact`, `list_artifacts`,
`list_versions`, `get_version`, `revert_artifact`, `delete_artifact`).
Auth is the same bearer token via the `Authorization` header:

```
claude mcp add --transport http artifact-bin https://artifacts.minusxapi.com/mcp \
  --header "Authorization: Bearer mx_..."
```

### Version history & revert (undo a bad edit)

Every `PUT` archives the previous state. To roll back:

```
GET  https://artifacts.minusxapi.com/api/artifacts/<id>/versions   → 200 { "versions": [ { "version", "title", "created_at" } ] }
POST https://artifacts.minusxapi.com/api/artifacts/<id>/revert     { "version": 1 } → 200 { "id", "slug", "url", "version": <new> }
```

A revert creates a NEW version (the pre-revert state is archived too), so
reverts are themselves undoable and the link never changes.

### Screenshot / export as an image (no auth, curlable)

```
GET https://artifacts.minusxapi.com/a/<slug>?export=png   → image/png of the fully rendered page
GET https://artifacts.minusxapi.com/a/<slug>?export=jpg   → image/jpeg
```

Rendered on demand in a server-side headless browser — full page at 1200px
wide, repeat fetches cached until the artifact changes, nothing stored. Use it
to eyeball your own output or hand your user a static image:

```bash
curl -sS -o report.png "https://artifacts.minusxapi.com/a/<slug>?export=png"
```

Share pages also carry `og:image` pointing at this URL, so links pasted into
Slack and the like unfurl with a live preview. A `503 render_unavailable`
means this deployment has no headless browser installed — the HTML link still
works.

### Delete an artifact

```
DELETE https://artifacts.minusxapi.com/api/artifacts/<id>
→ 200 { "ok": true }
```

Permanent: the public link dies and version history is erased. Confirm with
your user before deleting anything they shared.

## Rules for artifact HTML (the `html` tier)

Artifact pages are served with a strict Content-Security-Policy: **all network
access is blocked**. Anything external will silently fail to load.

- ONE self-contained file. Inline all CSS in `<style>` and all JS in `<script>`.
- No CDN `<script src>`, no external stylesheets, no web fonts, no `fetch`/XHR.
- Images/media only as `data:` URIs.
- The page is sandboxed: forms and popups won't work; design content to be self-contained.
- Max size: 2,000,000 bytes (~2 MB) including embedded data: URIs.

## Errors

| Status | Meaning | What to do |
|---|---|---|
| 400 | `invalid_json` / `one_of_markdown_html_markup_jsx` / `invalid_jsx` / `invalid_refs` / `unknown_theme` | Fix the request body — `details` names each problem with its span |
| 401 | `unauthorized` | Token wrong/revoked — ask your user, don't retry |
| 403 | `quota_exceeded` | This token is at its artifact cap — delete something or use another token |
| 404 | `not_found` | No artifact with that id belongs to your token |
| 409 | `version_conflict` | Your `expectedVersion` is stale — re-read, merge, retry with `currentVersion` |
| 409 | `has_dependents` | Other documents reference this artifact — re-send DELETE with `?force=true` to break them knowingly |
| 413 | `too_large` | Shrink the content (max 2,000,000 bytes) |
| 429 | `rate_limited` | Back off and retry after a minute |

## Typical workflow

1. `POST` once → share the returned `url` with your user.
2. For revisions, `PUT` the same id — the link stays stable, the version bumps.
3. `GET` the id first when you need the current HTML to edit from.

## TL;DR — every link in one place

Docs:

- `https://artifacts.minusxapi.com/api` — this document (also at `https://artifacts.minusxapi.com/api/llm-docs`)
- `https://artifacts.minusxapi.com/api/markup` — the markup (story JSX) reference (READ before authoring markup)
- `https://artifacts.minusxapi.com/docs` — the human-readable tour (send your user here)

API:

- `POST https://artifacts.minusxapi.com/api/tokens/anonymous` — mint a token (no auth)
- `https://artifacts.minusxapi.com/mcp` — MCP server (same token, same operations)
- `POST https://artifacts.minusxapi.com/api/artifacts` — create (`markup` | `markdown` | `html` | `dataset` | `viz` | `image`)
- `GET https://artifacts.minusxapi.com/api/artifacts` — list yours
- `GET https://artifacts.minusxapi.com/api/artifacts/<id>` — read one back (source + theme)
- `PUT https://artifacts.minusxapi.com/api/artifacts/<id>` — replace content, bump version, same link
- `DELETE https://artifacts.minusxapi.com/api/artifacts/<id>` — permanent delete (confirm with your user)
- `GET https://artifacts.minusxapi.com/api/artifacts/<id>/versions` — version history
- `POST https://artifacts.minusxapi.com/api/artifacts/<id>/revert` — roll back to a version
- `https://artifacts.minusxapi.com/a/<slug>` — the public share page (the deliverable)
- `https://artifacts.minusxapi.com/a/<slug>?export=png` — the page as a PNG (also `jpg`; curlable, no auth)
- `https://artifacts.minusxapi.com/e/<id>` — the human editor for an artifact
