Skip to content

Accept and echo Octi-Device-Capabilities header#23

Merged
d4rken merged 2 commits into
mainfrom
feat/device-capabilities
May 18, 2026
Merged

Accept and echo Octi-Device-Capabilities header#23
d4rken merged 2 commits into
mainfrom
feat/device-capabilities

Conversation

@d4rken
Copy link
Copy Markdown
Member

@d4rken d4rken commented May 18, 2026

Summary

Adds server support for the per-device capability tag set introduced client-side in d4rken-org/octi#309. The server is intentionally a dumb pipe: it parses, validates, persists, and echoes — no knowledge of what individual tag values mean.

Until clients ship the header, this is a no-op (no peer sends Octi-Device-Capabilities, so nothing changes). Once the Android client (post-#309) is deployed and other platforms follow suit, the data flows end-to-end and Android peers see each other's capabilities via the existing device-list endpoint.

Wire contract

  • Header: Octi-Device-Capabilities: ["encryption:_reported","encryption:AES256_GCM_SIV","encryption:AES256_SIV"]
  • Tag shape: <namespace>:<value> — ASCII, lowercase namespace, regex [a-z][a-z0-9]*:[A-Za-z0-9._\-]+
  • Limits: max 64 tags, max 128 chars per tag, max 4096 chars in header value
  • Response: included as a JSON array on each device in GET /v1/devices (proper object, not stringified)

Behavior

When Action
Octi-Device-Capabilities header present on a state-updating authenticated request Parse + validate + store on the device
Header malformed (bad JSON, wrong shape, oversized, non-array, bad tag) WARN log, treat as absent — no state change
Header absent on a state-updating request No change (matches existing behavior for version/platform/label)
GET /v1/devices Each device's capabilities field carries its currently-stored set, or null if never reported

Honored on the same routes that already honor Octi-Device-Version / Octi-Device-Platform / Octi-Device-Label:

  • POST /v1/account (register / link)
  • Heartbeat via touchAuthenticatedDevice (any authenticated state-updating request through HttpExtensions.kt)
  • WebSocket connect (/v1/ws)

Stale-state caveat

Absent-header-on-update follows the existing pattern for label/version/platform: previously-stored capabilities are preserved. If a device downgrades to a client that no longer sends the header, its capabilities remain stale until something explicitly clears them. Acceptable trade-off vs. forcing every request to declare them.

If this becomes a real problem (downgrade scenarios in the wild), a follow-up could clear capabilities whenever the device's version or platform changes without an accompanying Octi-Device-Capabilities header.

Validation rationale

The validation mirrors the client-side codec in octi#309 — same regex, same limits. Server-side validation is defense in depth: a misbehaving client (or hostile substrate) could otherwise pollute on-disk JSON and amplify peer-side error logs. On any bad tag the whole set is rejected (no partial acceptance) so consumers get either valid data or nothing — no surprise dropped tags.

Test plan

  • ./gradlew test — all pass.
  • New ParseCapabilitiesHeaderTest covers each rejection path: null/blank, malformed JSON, non-array, non-string element, bad tag shape, oversize set, oversize tag, oversize header. Plus happy-path roundtrip and empty-array → empty-set.
  • DeviceFlowTest extended: capabilities stored at register; updated on authenticated request; preserved when header absent; rejected on malformed JSON / bad tag / non-string / oversize; empty array → empty set.
  • CorsFlowTest.preflight updated so the SPA can include Octi-Device-Capabilities in its preflight.
  • Manual integration with the Android client running octi#309 once this is deployed.

Adds support for a new per-device capability tag set published by clients via the Octi-Device-Capabilities HTTP header. The server is a dumb pipe — it parses, validates (max 64 tags, 128 chars each, ASCII namespace:value shape), stores per device, and echoes back in the device-list response. No knowledge of tag semantics required.

Honored on every state-updating authenticated request that already updates version/platform/label: register, heartbeat (via touchAuthenticatedDevice), WebSocket connect. Absent header = no update (matches the existing pattern for label/version/platform). Malformed headers are dropped (treated as absent) with a WARN log; entire set is rejected on any bad tag for wire-format consistency.

Activates the client-side capability mechanism shipped in octi#309. Until clients send the header, this is a no-op; once they do, the data flows end-to-end. CORS allowlist updated so SPAs can send the header in preflight.
@d4rken d4rken added the enhancement New feature or request label May 18, 2026
Two coverage gaps spotted in review of the existing capabilities tests:

- WsFlowTest: open a WS handshake with the capabilities header and verify the device's stored set reflects it. The wiring lives in WsRoute, not HttpExtensions.verifyCaller, so the existing HTTP-only tests didn't reach it.

- DeviceFlowTest: two-device test where device 2 joins via share code with capabilities, device 1 reads them via the devices list endpoint. Pins the user-visible peer-echo contract that the client-side feature depends on.
@d4rken d4rken merged commit e13fe60 into main May 18, 2026
5 checks passed
@d4rken d4rken deleted the feat/device-capabilities branch May 18, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant