feat(shard)!: declare wire protocol 3

The site's declared version is the admin-set uo_link_config.protocol column, so
the sidecar's PROTOCOL_VERSION 2 -> 3 bump has to be matched here or every REST
call 409s and uoLinkSocket closes the WS on the ws.hello mismatch. Five places
carry the number and all five move together: the column default, the model's
DEFAULT_PROTOCOL (what a site with nothing saved yet declares), the two
`config.protocol || 1` fallbacks in uoLinkClient/uoLinkSocket -- unreachable
today, but an unset value quietly sending 1 is exactly the confusing 409 the
version check exists to prevent -- the admin form's initial value, and the
documented env default.

The boot migration is the only subtle part. schema.sql is re-run on EVERY boot,
and `protocol` is admin-editable, so a bare UPDATE would silently un-pin an
operator who had deliberately pinned an older sidecar in Admin -> Shard. It is
therefore gated on a marker row in `settings`, written after the UPDATE: the
first boot on this build migrates, every later boot is a no-op. `protocol < 3`
rather than `= 2` picks up an install still on the old default of 1, which could
not have been talking to a v2 sidecar anyway. A fresh install has no row to
update and just gets the marker plus the new column default.

Verified against the local MariaDB through ensureSchema (the production path):
2 -> 3 with the marker written and the column default now 3; pinned back to 2 by
hand, re-ran, and it STAYED 2 -- the one-shot property holds. 673 server tests,
47 client tests, client build green.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-29 18:03:48 -05:00
parent c6c0c257dd
commit 779a304173
6 changed files with 29 additions and 7 deletions

View File

@@ -7,7 +7,10 @@
const db = require('./uoLinkConfig.db')
const secretBox = require('../../utils/secretBox')
const DEFAULT_PROTOCOL = Number(process.env.UOLINK_PROTOCOL) || 1
// The wire protocol this build speaks (link/sidecar/src/main.rs PROTOCOL_VERSION).
// Only used before an admin has saved anything — the stored row wins once it exists,
// and UOLINK_PROTOCOL still overrides for an operator running an older sidecar.
const DEFAULT_PROTOCOL = Number(process.env.UOLINK_PROTOCOL) || 3
function toSafe(row) {
if (!row) {

View File

@@ -58,7 +58,7 @@ async function call(path, { method = 'GET', body } = {}) {
const headers = {
'Content-Type': 'application/json',
'X-UOLink-Version': String(config.protocol || 1),
'X-UOLink-Version': String(config.protocol || 3),
}
if (config.token) headers.Authorization = `Bearer ${config.token}`

View File

@@ -197,7 +197,7 @@ async function connect() {
return
}
state.protocol = config.protocol || 1
state.protocol = config.protocol || 3
helloSeen = false
const url = buildUrl(config.wsUrl, config.token)