mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
* feat(auth): authenticate daemon clients through hello Give local CLI and desktop clients a per-run credential while remote clients send the host password inside the protocol. Keep relay passwordless admission for this mobile release so existing saved hosts keep working until the follow-up enforcement release. Move password recovery for saved hosts into host settings and keep pairing credentials in the add flow. Include isolated web QA evidence for both flows. * fix(auth): preserve Hub bootstrap and local credential expectations Hub-owned sockets already carry admission; keep their hello handling synchronous through session bootstrap while resolving credentials asynchronously for ordinary sockets. Keep the resolver out of lightweight configuration imports. Update CLI expectations for same-home credentials, let a rejected connection yield to another usable connection, and clear pairing secrets when add-flow targets change. * fix(pairing): retain passwords across edits to the same target Track the last valid pairing host while a URI is edited. This preserves an entered password for whitespace and partial edits but clears it when a different host becomes the target. * test(auth): align local credential checks across platforms Expect the CLI to authenticate through the credential in its selected home, and verify POSIX file modes only where Node exposes them. * feat(app): move host password into a settings row with a modal The host page rendered an always-visible password form above Name on every host. Replace it with a Password row under a Security section below Appearance; tapping it opens a modal with Save and, when a password is saved, Clear. The connection-error callout keeps its guidance and gains a Set password button that opens the same modal. Drop the unused pairing.hostPassword body/reopen/saveError strings. * chore: remove committed QA screenshots QA evidence stays out of the repository; the PR body describes it in words. * feat(app): keep the host password out of the host page The saved password is connection data, entered only when a host is added. A Password setting on the host page reads as the daemon's own password, so the row, modal, and callout button are gone. A rejected host shows the daemon's reason with guidance to remove the host and add it again with the password. useHostMutations no longer exposes setHostPassword; relay imports still set it inside the runtime. * feat(app): show host connection errors in an error alert HostConnectionError renders the existing Alert with the connection error as its title. Auth rejections add the re-add guidance as the description; other errors show the title only. The Overview and Connections pages share it. * style(app): soften alert borders to half-opacity accent * feat(app): size alerts on the button scale Alert takes a size prop (xs/sm/md/lg, default md) whose geometry lives next to the button's in control-geometry: one font size for title and description, the button icon size centered on the first text line, button padding and radius, and a one-line alert as tall as the button of the same size. Consumers pick sm among settings rows, cards, panels, and modals, and md when the alert is the whole page or sheet state. * style(app): roomier, rounder alerts with a tight title block Alert sm padding returns to the original 12/16 and every size is at least borderRadius.xl. Title and description sit a small gap apart with no line height override. The icon slot carries a zero-width text strut in the alert's font so the icon centers on the first line for titled, title-only, and description-only alerts. * refactor(app): align the alert icon with a centered lead row Drop the zero-width strut. The icon and the alert's first text (title, else description) share one row centered on each other; the description and actions sit below, indented by the icon slot width plus gap so they start at the lead text's left edge. * refactor(app): drop the unused Alert icon override
96 lines
6.4 KiB
Markdown
96 lines
6.4 KiB
Markdown
# Protocol Compatibility
|
|
|
|
The app and the daemon are separate products that ship separately. A user updates the app from an app store or a desktop auto-update; they update the daemon when they feel like it. Every combination happens in the wild: new app against an old daemon, old app against a new daemon, and both sides months apart.
|
|
|
|
In development both sides are always the same version, which is why this is the constraint contributors miss most often.
|
|
|
|
Two contracts follow from it.
|
|
|
|
## The protocol contract: always compatible
|
|
|
|
A schema change must not break parsing in either direction. An old app still parses messages from a new daemon. A new daemon still parses messages from an old app.
|
|
|
|
`hello` carries an optional credential. The daemon accepts a client protocol version from its minimum through newer client versions, selects the lower of client and daemon maximum, and reports it as optional `server_info.protocolVersion`. A `hello.rejected` frame precedes an auth close only when the client sent `hello.auth` or advertised `hello_rejection`; older clients receive the existing WebSocket close code and reason. This avoids sending them a new top-level message their validator does not recognize.
|
|
|
|
- New fields are `.optional()` with a sensible default.
|
|
- Never flip optional to required, remove a field, or narrow a type. `string` to `enum` and nullable to non-null are both narrowing.
|
|
- A field you stop sending stays accepted. You stop writing it, you don't stop reading it.
|
|
- Wire schemas are pure structural declarations. No `.transform()`, `.catch()`, or `.preprocess()` on WebSocket message schemas — normalization happens in an explicit pass after validation. The reason is in [protocol-validation.md](protocol-validation.md): inbound validators are generated, and the generator only compiles pure schemas.
|
|
- Plain `z.union()` is forbidden when every branch shares a literal tag. Use `z.discriminatedUnion()`.
|
|
- `.default()` belongs on primitive leaves only, never on item schemas inside large arrays or big inbound containers.
|
|
|
|
Two questions to ask before you commit a schema change:
|
|
|
|
1. Does a six-month-old app still parse this message?
|
|
2. Does a six-month-old daemon still send something this app accepts?
|
|
|
|
If you can't answer both with yes, the change isn't done.
|
|
|
|
Schemas live in `packages/protocol/src/messages.ts`. New RPC names follow [rpc-namespacing.md](rpc-namespacing.md).
|
|
|
|
## The feature contract: per feature, gated once
|
|
|
|
Features don't have to work across versions. A new feature usually needs a new daemon capability, and old daemons don't have it.
|
|
|
|
The app checks for the capability and either runs the feature or tells the user to update the host.
|
|
|
|
- **No fallback paths.** Don't build a degraded version of the feature for old daemons. Don't fan out across legacy RPCs to simulate a capability that isn't there. The user updates or doesn't get the feature.
|
|
- **No defensive branches spread through the feature.** Detection happens in one place, and everything downstream reads a clean shape.
|
|
- Capability flags live in `features` on the `server_info` message (`packages/protocol/src/messages.ts`, the `server_info` schema).
|
|
|
|
Existing functionality keeps working across versions because of the protocol contract. Gating a new feature never substitutes for that.
|
|
|
|
## Client capability ownership
|
|
|
|
The client package advertises the protocol behavior it implements. Add each new capability to
|
|
its exhaustive defaults and implement the associated subscription or decoding behavior there.
|
|
The app, CLI, and plugins inherit those defaults; they supply only host resources such as browser
|
|
automation, or explicit overrides. A schema accepting a message does not establish support for
|
|
its delivery semantics.
|
|
|
|
On capable daemons, connecting creates no timeline or event demand. Client subscriptions own their network membership,
|
|
release it on unsubscribe, and restore it after reconnect. Raw message observers inspect traffic
|
|
without requesting streams. Application caches and which agents are visible remain caller-owned.
|
|
|
|
## Owned observations
|
|
|
|
`owned_subscriptions` and `server_info.features.ownedSubscriptions` negotiate the source-owned
|
|
contract described in [architecture](architecture.md#websocket-protocol). The client selects delivery
|
|
behavior once at its connection boundary. App workflows use the same observation interface on both.
|
|
|
|
With an older daemon, the client uses the existing connection and legacy RPCs. Directory subscriptions
|
|
remain shared and last-query-wins. Local handle IDs identify listeners; they do not promise independent
|
|
server filters. Timeline and event membership retain their existing shared behavior. Releasing a handle
|
|
detaches its listener and uses the old unsubscribe operation where one exists. Broadcast-only hosts keep
|
|
broadcasting; readiness there means local attachment, not a daemon acknowledgement. No extra sockets
|
|
or multiplexing emulation are introduced.
|
|
|
|
Preserve established workflows when changing delivery internals. Independent filters and quiet
|
|
connections require a capable daemon; opening the app, reading history and using terminals do not.
|
|
Pre-registry workspace grouping and legacy event normalization belong inside the client boundary.
|
|
|
|
Old clients keep their existing wire shapes and slot behavior at the daemon's source boundary.
|
|
The adapter keys legacy slots by physical socket, so an old connection cannot replace a modern
|
|
sibling's observation even when both use the same logical client ID. Optional wire IDs stay accepted
|
|
for parsing compatibility; modern requests cannot select their subscription ID.
|
|
|
|
## Every shim is tagged and dated
|
|
|
|
A shim that exists for old-app or old-daemon support carries a comment naming it, the version it arrived in, and when it can go:
|
|
|
|
```ts
|
|
// COMPAT(workspaceFileEditing): added in v0.2.0, remove after 2027-01-18 once daemon floor >= v0.2.0.
|
|
```
|
|
|
|
`rg "COMPAT\("` is the full cleanup backlog, so:
|
|
|
|
- One tag per shim, at the site that has to be deleted.
|
|
- Give it a name, a version, and a removal condition or date. Six months out is the usual default.
|
|
- Never bury compatibility in an untagged `??` fallback or an optional-chain tunnel. Untagged back-compat never gets removed, because nobody can find it.
|
|
|
|
When a tag's condition is met, delete the shim and the tag in the same change.
|
|
|
|
## QA
|
|
|
|
Tests don't fully cover compatibility. If you touched `packages/protocol`, say in the pull request why an older app still parses your message and why an older daemon still satisfies your app. See [qa.md](qa.md).
|