release(v4.1.0): browser-friendly HTTP RPC for @shade/files
Default shade.files.client(peer) requires both peers to be mutually
addressable over HTTP — the response round-trips through
Shade.deliverControlEnvelope (POST to peer's /v1/transfer/control).
Browser tabs can't host an HTTP server, so they couldn't consume
@shade/files at all. Dispatch's filutforsker (admin-panel browser UI)
is the canonical use-case.
This release adds a parallel request-response transport: one POST per
RPC, encrypted envelope in the body, encrypted response in the same
HTTP response. No inbound channel needed on the client.
### New API
- shade.files.rpcRoute(opts?) — Hono app exposing POST /rpc.
- shade.files.httpClient(peer, opts) — request-response FileClient.
- FilesNamespace.serve(handler, { inlineOnly: true }) — skip streams-
bridge (and its configureTransfers pre-condition); also skip
channel-based dispatch so requests aren't double-dispatched.
### Limitations (v1)
Inline only (≤ 256 KiB). Streamed reads/writes throw clear errors
directing to shade.files.client(peer) on a server-to-server deploy.
### Tests
7 integration tests in tests/integration/http-rpc.test.ts covering
round-trip + negative cases (sender header, empty/garbage body,
maxBodyBytes, rpcRoute-without-serve).
### Symmetry
Mirrors @shade/server's shade-auth-middleware: encrypted envelope in
request body, decrypted via existing ratchet, response in same HTTP
roundtrip. No WebSocket, no SSE, no outbound from server.
Wire-compatible. Source-compatible. Lockstep bump to 4.1.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
97
CHANGELOG.md
97
CHANGELOG.md
@@ -5,6 +5,103 @@ All notable changes to Shade are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [4.1.0] — 2026-05-03 — Browser-friendly HTTP RPC for @shade/files
|
||||
|
||||
The default `shade.files.client(peer)` requires both peers to be
|
||||
mutually addressable over HTTP — the response to a `list` / `read` /
|
||||
etc. round-trips through `Shade.deliverControlEnvelope`, which POSTs
|
||||
to the peer's `/v1/transfer/control` endpoint. **That doesn't work
|
||||
for browsers** — a tab can't host an HTTP server, so the server
|
||||
cannot call back outbound.
|
||||
|
||||
This release ships a parallel request-response transport. One POST per
|
||||
RPC, encrypted envelope in the request body, encrypted response in the
|
||||
same HTTP response. Mirrors the way `@shade/server`'s
|
||||
`shade-auth-middleware` works for prekey writes.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/files`
|
||||
- `createFilesRpcRoute(shade, handler, options?)` — Hono app exposing
|
||||
`POST /rpc`. Reads `X-Shade-Sender-Address`, decrypts the envelope
|
||||
via the existing ratchet session, dispatches through the attached
|
||||
`FileHandler`, encrypts the result, and returns it in the same HTTP
|
||||
response. Transport-level failures (no session, undecryptable, body
|
||||
too big) return JSON `{ error }` with appropriate 4xx; application-
|
||||
level failures ship encrypted `RpcError` envelopes.
|
||||
- `createFilesHttpClient(shade, peer, options)` — request-response
|
||||
`FileClient` for browser-style consumers. Each method (list / stat /
|
||||
mkdir / delete / move / getThumbnail / custom / write inline / read
|
||||
inline) does one HTTP POST and parses the encrypted response. No
|
||||
inbound channel required.
|
||||
- `shade.files.rpcRoute(opts?)` — namespace-side getter for the route.
|
||||
Throws if no handler has been attached via `shade.files.serve(...)`
|
||||
first.
|
||||
- `shade.files.httpClient(peer, opts)` — namespace-side getter for the
|
||||
client.
|
||||
- `FilesNamespace.serve(handler, { inlineOnly: true })` — opt-out flag
|
||||
that skips the streams-bridge setup. Required for HTTP-RPC-only
|
||||
servers (which don't need `configureTransfers({ resolveBaseUrl })`).
|
||||
In `inlineOnly` mode the channel-based dispatcher is also not
|
||||
attached, so requests are dispatched only by the rpc-route — avoids
|
||||
double-dispatch when a browser client and a server-to-server client
|
||||
share the same Shade instance.
|
||||
- `ShadeBridge` (exported) gains a `receive(peer, envelope)` member
|
||||
matching `Shade.receive` so server-side rpc-route can decrypt
|
||||
inbound envelopes through the structural surface.
|
||||
|
||||
### Wire contract
|
||||
|
||||
```
|
||||
POST /rpc HTTP/1.1
|
||||
Content-Type: application/octet-stream
|
||||
X-Shade-Sender-Address: alice@example.com
|
||||
|
||||
<wire-encoded ShadeEnvelope wrapping JSON-encoded RpcRequest>
|
||||
|
||||
────
|
||||
|
||||
200 OK
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
<wire-encoded ShadeEnvelope wrapping JSON-encoded RpcResponse | RpcError>
|
||||
```
|
||||
|
||||
### Limitations (v1)
|
||||
|
||||
- **Inline payloads only** (≤ 256 KiB). `write` of larger inputs
|
||||
throws `ConflictError` directing callers to `shade.files.client(peer)`
|
||||
on a server-to-server deployment. Streamed `read` results throw
|
||||
`InternalFileError` for the same reason.
|
||||
- The X3DH first-message must ride the same RPC route — set
|
||||
`acceptFirstMessage: true` on `rpcRoute({ acceptFirstMessage: true })`
|
||||
when the browser client's first-ever call doubles as the handshake.
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/integration/http-rpc.test.ts` — round-trip via HTTP
|
||||
(list / mkdir / stat / write / read / delete) plus negative cases
|
||||
(streamed write rejected, missing sender header, empty body, garbage
|
||||
body, body past `maxBodyBytes`, `rpcRoute()` without `serve()`).
|
||||
|
||||
### Migration
|
||||
|
||||
`4.0.x → 4.1.0` is wire-compatible and source-compatible. The HTTP
|
||||
RPC route is purely additive — no existing code path changes. To
|
||||
adopt:
|
||||
|
||||
```ts
|
||||
// server (was)
|
||||
await shade.files.serve(handlerConfig);
|
||||
|
||||
// server (HTTP-RPC mode)
|
||||
await shade.files.serve(handlerConfig, { inlineOnly: true });
|
||||
app.route('/api/v1/shade-files', shade.files.rpcRoute());
|
||||
|
||||
// browser client
|
||||
const fs = shade.files.httpClient(serverAddress, { rpcUrl: '...' });
|
||||
```
|
||||
|
||||
## [4.0.2] — 2026-05-03 — Consumer-strict reader-shape fixes
|
||||
|
||||
`4.0.1` shipped the `tsc --noEmit` gate that compiles each package
|
||||
|
||||
Reference in New Issue
Block a user