# service.lyabah.com An internal service for storing files and text, and for passing data between people, scripts and AI agents. Base URL: `https://service.lyabah.com` - **`/files/…`** stores any file (images, PDFs, archives, binaries) in object storage. Downloading answers with a redirect to a temporary storage link. - **`/wiki/…`** stores text (JSON, YAML, XML, HTML, Markdown, CSV, plain text) and serves it back as-is with the right content type. Every change is kept in the page's history. - **`/stats`** shows usage. **`/help`** is this page; **`/help.md`** (also `/llms.txt`) is the same text as plain Markdown, for agents. ## Authentication Every write, every listing and every read of a private item needs the service key, sent as a header: Authorization: Bearer $LYABAH_KEY Items are **private by default**. Add `public=true` to an upload (or `PATCH` it later) to let anyone with the URL read it — for example an image embedded in a GitHub README. The key is accepted only in that header: never put it in a URL. ## Quick start ```sh KEY="Authorization: Bearer $LYABAH_KEY" # Upload a private file, and a public image. curl -H "$KEY" -T report.pdf https://service.lyabah.com/files/docs/report.pdf curl -H "$KEY" -T chart.png "https://service.lyabah.com/files/img/chart.png?public=true" # Download: follow the redirect. curl -L -H "$KEY" -o report.pdf https://service.lyabah.com/files/docs/report.pdf # Write a wiki page, read it back, see its history. curl -H "$KEY" -T config.json https://service.lyabah.com/wiki/project/config.json curl -H "$KEY" https://service.lyabah.com/wiki/project/config.json curl -H "$KEY" "https://service.lyabah.com/wiki/project/config.json?history" ``` ## Paths `/files/` and `/wiki/`, where `` is `folder/…/name`. - Each segment uses letters, digits, `.`, `_` and `-` only — no spaces, no `%`-escapes. Paths are case-sensitive and at most 1024 characters. - Folders are implicit, as in S3: a folder exists while something is in it, and folders nest (`a/b/c/file.txt`). A file may also sit at the root. - A path that ends in `/` is the folder itself, which is how you list it. `/files/` and `/wiki/` list the root. ## Files | Request | What it does | |---|---| | `PUT /files/` | Upload the request body: any bytes, up to 100 MB. Creates or replaces. | | `GET /files/` | `302` redirect to a temporary download link, valid for 1 hour. `HEAD` works too. | | `GET /files/?meta` | Metadata, as JSON. | | `GET /files//` | List a folder. | | `PATCH /files/` | Change visibility or expiry; move or rename. | | `DELETE /files/` | Delete (`204`). | ### Upload ```sh curl -H "$KEY" -T build.tar.gz "https://service.lyabah.com/files/builds/app-1.4.tar.gz?expires_in=7d" ``` Query parameters, all optional: - `public=true` or `public=false` — who may read it. A new file is private. - `expires_in=30m|12h|7d|2w` or `expires_at=2026-12-31T23:59:00Z` — delete it automatically after that. `expires_in=never` removes an expiry. Replacing a file keeps its visibility and expiry unless the upload sets them. Send `If-None-Match: *` to refuse to replace an existing file (`412`). The stored content type comes from your `Content-Type` header when you send a specific one. Otherwise it comes from the file extension (`.png`, `.pdf`, `.zip`, `.tar.gz`, `.json`, …), and otherwise from sniffing the bytes. `curl -T` sends no content type, which is fine. Bytes are stored exactly as sent, so archives come back byte-for-byte. The answer is `201` for a new file, `200` for a replaced one, with the metadata: ```json { "path": "builds/app-1.4.tar.gz", "folder": "builds", "name": "app-1.4.tar.gz", "url": "https://service.lyabah.com/files/builds/app-1.4.tar.gz", "size": 1048576, "content_type": "application/gzip", "sha256": "9f86d0…", "public": false, "expires_at": "2026-10-05T12:00:00Z", "downloads": 0, "created_at": "2026-09-28T12:00:00Z", "updated_at": "2026-09-28T12:00:00Z" } ``` Uploads pass through a proxy that ends any request after 60 seconds, so a large file over a slow link can fail; 100 MB is the hard limit (`413`). ### Download `GET` answers `302` with a `Location` header pointing at a signed storage URL that works for 1 hour. Follow it (`curl -L`). Public files need no key. - Images, PDFs, audio, video and text open in the browser. Archives and other binaries download under their own name. - Your client must not send `Authorization` to the storage host. curl, Python `requests` and Go drop it on a redirect to another host by themselves; with curl, never use `--location-trusted`. - To get a temporary link to hand to something else, without downloading: `curl -s -o /dev/null -w '%{redirect_url}' -H "$KEY" https://service.lyabah.com/files/docs/report.pdf` - Always share the `https://service.lyabah.com/files/…` URL itself, not the storage link: the storage link expires. ### List ```sh curl -H "$KEY" https://service.lyabah.com/files/img/ ``` ```json { "folder": "img", "files": [ { "path": "img/chart.png", "size": 20480, "public": true, "…": "…" } ], "folders": [ "img/icons/" ] } ``` - `recursive=true` lists everything below the folder, with no `folders` part. - `limit=N` (default 1000, at most 10000). When there is more, the answer has `"next": ""`; pass it back as `after=` for the next page. ### Change, move, delete `PATCH` takes a JSON body. Give any of the fields; the rest stay as they are. ```sh # Make it public. curl -X PATCH -H "$KEY" -d '{"public": true}' https://service.lyabah.com/files/img/chart.png # Expire in 3 days; "expires_at": null removes the expiry. curl -X PATCH -H "$KEY" -d '{"expires_in": "3d"}' https://service.lyabah.com/files/img/chart.png # Move to another folder, keeping the name (note the trailing /)… curl -X PATCH -H "$KEY" -d '{"move_to": "archive/2026/"}' https://service.lyabah.com/files/img/chart.png # …or move and rename. curl -X PATCH -H "$KEY" -d '{"move_to": "archive/chart-old.png"}' https://service.lyabah.com/files/img/chart.png curl -X DELETE -H "$KEY" https://service.lyabah.com/files/archive/chart-old.png ``` Moving onto an existing item answers `409` unless the body also has `"overwrite": true`. `"move_to": "/"` moves to the root. ## Wiki | Request | What it does | |---|---| | `PUT /wiki/` | Create or replace the page with the request body: UTF-8 text, up to 5 MB. | | `GET /wiki/` | The page's content, as-is, with its content type. `HEAD` works too. | | `GET /wiki/?history` | Revisions, newest first, as JSON. | | `GET /wiki/?rev=N` | The content of revision N. | | `GET /wiki/?diff=N` | Unified diff from revision N-1 to N. `?diff=A..B` compares any two; `0` is the empty page before revision 1. | | `GET /wiki/?meta` | Metadata, as JSON (always needs the key). | | `GET /wiki//` | List a folder (the list has `pages` instead of `files`). | | `PATCH /wiki/` | Same body as for files. Moving keeps the history. | | `DELETE /wiki/` | Delete the page and its history (`204`). | `PUT` takes the same `public`, `expires_in` and `expires_at` parameters as a file upload. It answers `201` for a new page, `200` otherwise, with the metadata plus `"revision"` and `"changed"`. ### Content type The page's type comes from your `Content-Type` header when you send one. Otherwise it comes from the extension: `.json`, `.yaml`/`.yml`, `.xml`, `.html`, `.md`, `.txt`, `.csv`, `.toml`, `.jsonl`, `.js`, `.css`, `.svg`. Anything else is `text/plain`. Pages are served with `; charset=utf-8`. A body that is not UTF-8 text is refused with `415`; store binaries under `/files`. ### Revisions and safe concurrent writes Every write that changes the text makes a new revision, numbered from 1. Writing the same text again makes none (`"changed": false`). Every answer carries `ETag: "N"` and `X-Revision: N`. Several agents writing one page should use a read–modify–write loop, so that nobody overwrites a change they have not seen: ```sh curl -s -D head.txt -o todo.md -H "$KEY" https://service.lyabah.com/wiki/team/todo.md # head.txt has ETag: "7" # …edit todo.md… curl -H "$KEY" -H 'If-Match: "7"' -T todo.md https://service.lyabah.com/wiki/team/todo.md ``` A `412` means someone else wrote first: read the page again, reapply your change and retry. `If-None-Match: *` on a `PUT` only creates, never replaces. To poll cheaply, send `If-None-Match: "7"` on a `GET`: the answer is `304` until the page changes. Optional headers `X-Author: ` and `X-Message: ` on a `PUT` are stored with the revision and shown in `?history`: ```json { "path": "team/todo.md", "revision": 8, "revisions": [ { "revision": 8, "created_at": "2026-09-28T12:00:00Z", "author": "planner-agent", "message": "add deploy step", "content_type": "text/markdown", "size": 812, "sha256": "…", "lines_added": 1, "lines_removed": 0 } ] } ``` ## Expiry Files and wiki pages can expire. Set `expires_in` (`30m`, `12h`, `7d`, `2w`) or `expires_at` (RFC 3339) as a query parameter on a `PUT`, or as a JSON field on a `PATCH`, where `"expires_at": null` removes it. An expired item disappears at once and is deleted for good within about a minute, with a wiki page's history. ## Errors Errors are JSON: `{"error": "what went wrong"}`. | Status | Meaning | |---|---| | `400` | Bad path, parameter or JSON body. | | `401` | The key is missing or wrong. | | `404` | No such item, or it has expired. | | `409` | A move target exists (send `"overwrite": true`), or two writes raced (retry). | | `412` | `If-Match` / `If-None-Match` did not hold. | | `413` | Body too large: 100 MB for files, 5 MB for wiki pages. | | `415` | Binary body sent to the wiki. | | `502` | Object storage failed. `GET /health/storage` (with the key) walks each storage operation and says which one fails and why. | ## Tips for agents - **Pass messages** through a wiki page: a JSON or JSONL page per channel, written with `If-Match` so that concurrent writers never lose an update, and read with `If-None-Match` to poll. Add `expires_in` to anything temporary. - **Show an image on GitHub**: upload it with `public=true` and use the `https://service.lyabah.com/files/…` URL in the Markdown. GitHub caches images, so upload a changed image under a new name to show the new one right away. - **Check before you overwrite**: `GET …?meta` returns the `sha256` of a file, and a wiki page's `ETag` is its revision number.