feat: attachment content types and the URL an attachment is embedded by - #8
Conversation
The server stores attachments now, and two facts about them are the library's to hold: which types a file can go up as, and where the bytes end up once it has. `attachmentsByExtension` is a second map rather than more rows in the first, because the two are read under opposite rules. An unknown extension on a page is a shrug that falls through to `text/html` — the case the tool is named for. On an upload the same shrug costs the upload: the bytes go up, the server validates them as UTF-8, and the 415 names a content type nobody chose. So `attachmentInferred` returns nil and the command refuses before spending the round trip. Sharing an entry point would have meant one of the two callers getting the other's answer. SVG is absent and should stay absent — it is the one image format that is also a document, the server does not accept it, and offering it here would turn a considered 415 into a type this tool appears to know. An attachment goes up through `publish`, which is not an omission: on the server it is a page whose body is bytes, same route, same namespace, and a second method here would invent a distinction the wire does not have. `filename` is the one parameter only an attachment may carry, and its ordinary failure is the silent one the `Query` enum exists for — `?flename=` earns a 201 and a download named after its slug. `bytesURL(for:)` is derived from the response rather than composed from `credential.host`, and that is the load-bearing decision. The server builds URLs from its own configured base, which need not be the address this client dialled; composing from the host we reached would hand back a URL that works from this machine and nowhere else, pasted into a page that outlives the machine. Scheme, host and port come from the server's answer and only the path is ours. It requires both rather than assuming them, because `URLComponents` parses `""` and a bare path quite happily and `/static/quiet-cedar-otter` looks enough like an answer to be printed. 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 (4)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThis change adds attachment MIME inference, binary attachment publishing with optional filenames, and byte URL construction under ChangesAttachment support
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to This PR adds attachment content-type handling and derives attachment URLs from the server response; no actionable merge-blocking risk remains in the supplied evidence. Sequence Diagram(s)sequenceDiagram
participant Caller
participant SteleClient
participant SteleServer
Caller->>SteleClient: publish binary attachment with content type and filename
SteleClient->>SteleServer: POST attachment content with query parameters
SteleServer-->>SteleClient: publish response
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/settings/billing. Comment |
The server stores attachments now, and two facts about them are the library's to hold: which types a file can go up as, and where the bytes end up once it has. This is the SteleKit half — no command yet, so nothing user-visible changes.
attachmentsByExtensionis a second map rather than more rows in the first, because the two are read under opposite rules. An unknown extension on a page is a shrug that falls through totext/html— the case the tool is named for. On an upload the same shrug costs the upload: the bytes go up, the server validates them as UTF-8, and the 415 that comes back names a content type nobody chose. SoattachmentInferredreturns nil and the caller can refuse before spending the round trip. Sharing an entry point would have meant one of the two callers getting the other's answer, which is whyinferredis untouched and still non-optional.SVG is absent and should stay absent. It is the one image format that is also a document, the server does not accept it, and offering it here would turn a considered 415 into a type this tool appears to know.
An attachment goes up through
publish, which is not an omission: on the server an attachment is a page whose body is bytes — same route, same namespace, and the verbs that already existed reach it unchanged. A second method here would invent a distinction the wire does not have.filenameis the one parameter only an attachment may carry, and its ordinary failure is the silent one theQueryenum exists for:?flename=earns a 201 and a download named after its slug.bytesURL(for:)is derived from the response rather than composed fromcredential.host, and that is the load-bearing decision here. The server builds its URLs from its own configured base, which need not be the address this client dialled — a deployment behind a proxy, or reached over a tailnet name, answers with the public one. Composing from the host we reached would hand back a URL that works from this machine and from nowhere else, pasted into a page that outlives the machine. So scheme, host and port come from the server's answer and only the path is ours. It requires both rather than assuming them:URLComponentsparses""and a bare path quite happily, and/static/quiet-cedar-otterlooks enough like an answer to be printed.Checks
swift test— 167 tests in 18 suites, all passing.swift build -c releaseclean, no warnings.New coverage: the attachment extension table and its refusal to guess; that the two maps stay separate in both directions; that SVG is off the list;
?filename=on the wire, alone and alongside?slug=/?ttl=; that no filename means no parameter; that a hostile filename stays one query value; and the fourbytesURLcases including a non-absolute viewer URL.🤖 Generated with Claude Code