feat: --ttl on publish, and a page whose deadline is no longer a secret - #2
Conversation
The server has had per-page expiry since stele-pages#17 and this client knew nothing about it. `PageLocation` decoded `slug` and `url` and dropped the `expires` the server sent, so `stele publish` printed a URL for a page that would stop being served in a matter of days and said nothing about it. The default is ephemeral by design — a page that outlives its purpose fails quietly and forever — but that only works if the caller is told. So: `--ttl <days|never>`, and the deadline printed under the URL. `PageTTL` is a second lifetime grammar and deliberately not `ExpiryDuration`. A credential's lifetime is seconds and spans seconds to weeks, so `90` there has two readings and guessing mints a credential that dies in ninety seconds; a page's is whole days, the server's own document teaches `?ttl=30`, and refusing the spelling an agent just read there would be a second grammar to no purpose. A bare number is therefore days. What is refused is `12h` — a good duration this server cannot store, honourable only by rounding to a lifetime nobody typed. It has no maximum. That bound is the server's `PageLifetime`, the way the slug rules are its `Slug`; a copy here would drift the day the server moved it, and the 400 names the real limit. What is checked locally is only what the server cannot check for us: that the input means a number of days, and that turning it into one does not overflow. The deadline is always read off the response, never computed. The server resolves it against its clock at upload, applies its own default when we say nothing, and on `update` reports a date this side never knew. `update` takes no --ttl at all: a page's lifetime is fixed at publish, and the server answers ?ttl= on PUT with a 400 rather than a 200 that ignored it. Two encoders earn their hand-written middle line. `PageLocation` emits an explicit null for a kept page because the synthesised `encodeIfPresent` drops the key, leaving `--json` unable to distinguish "no deadline" from "this tool has no opinion about deadlines" — the same reason the server hand-writes its own. Decoding treats null and absent alike, which is not a shortcut: a deployment older than page expiry omits the key, and nothing expires there either. The smoke test's run_stele stopped merging stderr into stdout. The split is a promise this tool makes — `url=$(stele publish page.html)` — and merging the streams is what would have hidden the deadline landing on the wrong one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThe change adds page lifetime support across the SDK, CLI, documentation, and integration tests. It models expiry metadata, validates and sends TTL values, reports deadlines on ChangesPage lifetime flow
Credential and route handling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CLI as PublishCommand
participant SDK as SteleClient
participant Server
participant Output as PageIO.report
CLI->>CLI: Parse --ttl
CLI->>SDK: Publish page with PageTTL
SDK->>Server: Send ttl query parameter
Server-->>SDK: Return PageLocation with expiresAt
SDK-->>Output: Return page location
Output-->>CLI: Write URL to stdout
Output-->>CLI: Write lifetime to stderr
Poem
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
…sserts `--no-default` was making the new host the default. `set` read `defaultHost` to decide whether one existed, but `encode` leaves the marker out of a one-host file, so the incumbent reloaded as nil and the host that declined took it. It now reads the incumbent — the marker, or the sole stored host — before inserting, and leaves an ambiguous file ambiguous. Slugs and client names are percent-encoded into their path segment. Left raw, dot-segment removal turned `update ../admin/clients` into `PUT /admin/clients`; the credential never left its own host, but the request was not for the resource that was named. Three smoke-script asserts were not asserting: `*"$(printf '\n')"*` is `*""*` and matched every string (aborting the run before the lifetime checks), an unknown option exits 64 rather than 1, and the `--ttl 90` comparison could pass on the empty output of a failed publish. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server has had per-page expiry since stele-pages#17 and this client knew nothing about it.
PageLocationdecodedslugandurland dropped theexpiresthe server sent — sostele publishprinted a URL for a page that would stop being served in a matter of days and said nothing about it.The ephemeral default is right: a page that outlives its purpose fails quietly and forever, while one that expires early fails loudly to somebody who can republish it. But that only works if the caller is told.
What this adds
--ttl <days|never>onpublish, and the deadline printed under the URL.stdout stays the bare URL —
url=$(stele publish page.html)still works. The deadline is on stderr.Two calls worth reviewing
Both diverge from the sibling
--expires-in, deliberately.A bare number is accepted here.
--expires-inrefuses one because its unit is seconds and its range spans seconds to weeks, so90has two readings and guessing mints a credential that dies in ninety seconds. A page's lifetime has one unit, the server's own skill document teaches?ttl=30, and refusing the spelling an agent just read there would be a second grammar to no purpose.12his refused rather than rounded. It is a perfectly good duration this server cannot store — the only ways to honour it are to round to a lifetime nobody typed, or to say so. The message names the unit, so the correction is arithmetic the caller can do.What this deliberately does not do
PageTTLhas no maximum. That bound belongs to the server'sPageLifetime, the way the slug rules belong to itsSlug— a copy here would be a second source of truth that drifts the day the server moves it, and the400it earns names the real limit. What is checked locally is only what the server cannot check for us: that the input means a number of days, and that turning it into one does not overflow.The deadline is always read off the response, never computed. The server resolves it against its clock at the moment of upload, applies its own default when we say nothing, and on
updatereports a date this side never knew.updatetakes no--ttlat all. A page's lifetime is fixed at publish, and the server answers?ttl=onPUTwith a400rather than a200that ignored it.Two hand-written encoders
PageLocationemits an explicitnullfor a kept page. The synthesisedencodeIfPresentdrops the key, which would leave--jsonunable to distinguish "no deadline" from "this tool has no opinion about deadlines" — the same reason the server hand-writes its own encoder.Decoding treats
nulland absent alike, which is not a shortcut: a deployment older than page expiry omits the key, and nothing expires on that server either. Both honestly mean "no deadline".One thing outside the feature
The smoke test's
run_stelewas merging stderr into stdout (2>&1), which would have swallowed the new deadline line into$OUTand broken the URL assertions. The streams are now split into$OUT/$ERR. No existing call site read stderr through$OUT— the one place that does, theauth loginTTY check, has its own inline invocation — and the split makes the stdout/stderr discipline testable, which it was not before.Testing
105 unit tests pass, covering the grammar, the sub-day refusal, the overflow paths, the
?ttl=wire spelling, and both JSON shapes for "no deadline".stele auth loginneeds a TTY. Its new legs — default lifetime is set,--ttl neveryields an explicit JSON null,--ttl 90differs from the default, the server rejects an over-long lifetime,update --ttlis refused — are written but have not run against a live server. Worth doing before merge.Still open
Both server-side features this CLI still lacks, neither addressed here: no
deletecommand despiteDELETE /pages/:sluglanding in stele-pagesc83463a, andScopehas nodeletecase.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Bug Fixes