Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ $ stele publish report.html --slug q3-report --ttl never
https://stele.example.com/q3-report
kept until deleted

$ stele attach chart.png --ttl never
https://stele.example.com/static/plain-amber-heron
kept until deleted
page about it: https://stele.example.com/plain-amber-heron

$ stele update q3-report report.html
https://stele.example.com/q3-report
kept until deleted
Expand All @@ -48,6 +53,13 @@ document. The deadline under the URL is on stderr for that reason, and it is pri
because the server's default is *ephemeral* — a page published with no `--ttl` is eventually
unpublished, and a caller handed only a URL would find that out when the link broke.

`stele attach` is the one command whose stdout is not the URL the server answered with. An
attachment has two: the bytes, under `/static/`, and a viewer page about the file. The bytes are
what a page embeds and the viewer is what you send a person, and mixing them up fails silently —
an `<img>` pointed at the viewer renders nothing, and both answer `200`. So the bytes go to
stdout, where `src=$(stele attach chart.png)` captures them, and the viewer goes to stderr
beneath the deadline. `--json` names both and calls neither of them `url`.

## Commands

| Command | Who runs it | Does |
Expand All @@ -56,6 +68,7 @@ unpublished, and a caller handed only a URL would find that out when the link br
| `stele auth status` | agent or human | Host, client name, scopes, expiry — never the token. |
| `stele auth logout` | human | Forgets the local credential. Does not revoke it. |
| `stele publish <file> [--slug <name>] [--ttl <days>]` | agent | `POST /pages`. Prints the URL on stdout and the page's deadline on stderr. |
| `stele attach <file> [--slug <name>] [--ttl <days>] [--filename <name>]` | agent | `POST /pages` with a binary type. Publishes an image, video or PDF. Prints the URL of the *bytes* on stdout — the one that goes in an `<img src>` — and the viewer's under the deadline on stderr. |
| `stele update <slug> <file>` | agent | `PUT /pages/:slug`. Never creates, and never changes the deadline. |
| `stele amend <slug> [--slug <name>] [--ttl <days>]` | agent | `PATCH /pages/:slug`. Renames a page, moves its deadline, or both, without touching a byte of it. A rename is a hard move — the old URL starts 404ing at once. |
| `stele delete <slug>` | agent | `DELETE /pages/:slug`. Permanent and immediate — the slug is freed and returns to the pool, so every link to it breaks. Prints nothing on stdout; the confirmation is on stderr. |
Expand Down Expand Up @@ -206,7 +219,7 @@ table.
| 3 | the server rejected the credential — ask the user to log in again |
| 4 | valid credential, insufficient scope — an operator has to run this |
| 5 | that slug is taken — ask for a different `--slug` |
| 6 | the page is too large or the wrong type |
| 6 | the file is too large or a type the server will not store |
| 7 | no such page or client |
| 8 | the CLI is too old — reinstall it and retry once |
| 9 | could not reach the server — retryable |
Expand Down Expand Up @@ -402,7 +415,7 @@ mint the first client. Arguments may also arrive as `STELE_SMOKE_HOST`, `STELE_S
| `--psql <command>` | optional | A command that reaches the server's database, e.g. `docker exec stele-postgres psql -U stele -d stele_smoke`. Only the attribution check needs it, because no route reports who wrote a page — without it that one check prints `skip`. |

It walks the whole documented lifecycle — mint, log in, `auth status`, publish, fetch the bytes
back and compare them, update, amend, `--expires-in` there *and* back, expiry actually enforced,
back and compare them, attach, update, amend, `--expires-in` there *and* back, expiry enforced,
a publish-only credential answered `200` by whoami and `403` by the admin routes, revocation that
really stops working, the exit-code vocabulary, the version gate, and the byte-identity of every
`404`. It prints each check, stops at the first failure, and exits non-zero.
Expand All @@ -424,13 +437,14 @@ Two things it does not do, stated because a canary you trust wrongly is worse th
refuses a pipe; the script asserts the *refusal* — which is the load-bearing custody rule — and
then seeds the credential file at `0600` in the documented format for the rest of the run. The
prompt, the echo-off read and the verify-before-write are not covered.
- **It leaves litter.** The server has a delete route but this CLI ships no command that reaches
it, so each run leaves eight pages and three
credential rows behind; it revokes every credential it mints, but a revoked row is still a row.
Five of those pages expire on their own — the lifetime checks publish several deliberately —
but three were published or amended to `--ttl never` and will still be there next year. The
footer names those three, since they are the only ones a human ever has to clean up. Run it
against a throwaway deployment, never against production.
- **It leaves litter.** Each run leaves ten pages and three credential rows behind. The pages it
deletes it deletes on purpose, to prove the verb works; the rest are the evidence the later
checks read, so they outlive the run. Credentials cannot be cleaned up at all — it revokes
every one it mints, but a revoked row is still a row. Seven of those pages expire on their own
— the lifetime checks publish several deliberately — but three were published or amended to
`--ttl never` and will still be there next year. The footer names those three, since they are
the only ones a human ever has to clean up. Run it against a throwaway deployment, never
against production.

It also moves `~/.config/stele/credentials.json` aside and restores it on every exit path,
including a failed check — see the `HOME` note under Configuration for why it cannot simply use a
Expand Down
7 changes: 7 additions & 0 deletions Sources/SteleKit/Inputs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public enum ContentType {
/// What `stele attach --content-type` offers at TAB.
public static var knownAttachments: [String] { Set(attachmentsByExtension.values).sorted() }

/// The same table read the other way, for the message a refused file earns.
///
/// Extensions rather than types, because they are what the person is holding. Told that a
/// deployment takes `image/png`, someone looking at `diagram.graffle` still has to work out
/// what to do; told the list is `png, jpg, …`, they already know.
public static var knownAttachmentExtensions: [String] { attachmentsByExtension.keys.sorted() }

/// The type to upload a file as, or nil when the extension says nothing.
///
/// Optional where `inferred` is not, and that difference is the whole point. `text/html` is
Expand Down
15 changes: 11 additions & 4 deletions Sources/SteleKit/SteleError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,23 @@ public enum SteleError: Error, Equatable, CustomStringConvertible {
name, or retire the existing one with `stele admin clients revoke <name>` and \
mint again — rotating a credential is revoke-then-mint under the same name.
"""
// Two limits behind one status, and the advice differs by which was hit. A page's is
// small and is usually blown by something that did not need to be inline; an
// attachment's is far larger and blowing it means the file is genuinely too big. This
// side cannot tell them apart — the status is the same and the command is not in scope
// here — so both remedies are named rather than the wrong one being guessed at.
case .pageTooLarge(let detail):
return """
the page is over the server's size limit\(Self.suffix(detail)) Drop inline \
images and link them instead — the limit is on the HTML, and a data: URI \
counts against it.
the file is over the server's size limit\(Self.suffix(detail)) For a page, \
publish images with `stele attach` and link them instead of inlining them — a \
data: URI counts against the HTML. For an attachment, it is over this \
deployment's ceiling, which is a much larger one.
"""
case .unsupportedContentType(let detail):
return """
the server will not store that content type\(Self.suffix(detail)) Publish one \
of the types it lists, or leave the type to the CLI.
of the types it lists, or leave the type to the CLI — pages and attachments \
accept different ones.
"""
case .upgradeRequired(let detail):
return """
Expand Down
2 changes: 1 addition & 1 deletion Sources/SteleKit/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
public enum SteleVersion {
/// Bumped by hand. Nothing derives it from git: a release built from a tarball or an
/// unclean tree must still report a version the server can compare.
public static let current = "0.3.0"
public static let current = "0.4.0"

/// Sent on every request. The `stele-cli/` product token is what the server matches on.
public static var userAgent: String { "stele-cli/\(current)" }
Expand Down
9 changes: 6 additions & 3 deletions Sources/stele/Exit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ enum Exit {
/// which is why the message carries the route's own advice and this table does not repeat it.
static let slugTaken: Int32 = 5

/// `413` / `415` — the page itself is the problem. Retrying is pointless; the input changes
/// or nothing does.
/// `413` / `415` — the file itself is the problem, whether it was a page or an attachment.
/// Retrying is pointless; the input changes or nothing does. The two are one code because
/// the caller's next move is the same shape either way, and `SteleError` carries the half
/// that differs: a page over the limit usually has something inline that `stele attach`
/// should have published, and an attachment over it is simply too big for the deployment.
static let pageRejected: Int32 = 6

/// `404` — an update to a slug that does not exist, an amendment or a deletion aimed at one
Expand Down Expand Up @@ -70,7 +73,7 @@ enum Exit {
(credentialRejected, "the server rejected the credential — ask the user to log in again"),
(forbidden, "valid credential, insufficient scope — an operator has to run this"),
(slugTaken, "that slug is taken — ask for a different --slug"),
(pageRejected, "the page is too large or the wrong type"),
(pageRejected, "the file is too large or a type the server will not store"),
(notFound, "no such page or client"),
(upgradeRequired, "the CLI is too old — reinstall it and retry once"),
(unreachable, "could not reach the server — retryable"),
Expand Down
Loading