deploy/2026.08.27.02 — a plan with commands, and a schema claim that is checked
2026.08.27.01 was REFUSED before a single host was contacted, and the commander was right twice over. its plan described the release instead of instructing the rollout — no step list at all, where deploy/README.md defines a plan as "what to run, in order, on which hosts" and steps 4 and 5 of the procedure are plan-driven. and it asserted "nothing here changes a schema" on the day dav/schema.ts was deleted out of core. that second one was not a formatting mistake. dav_app_passwords now lives in plugins/caldav, and gen-plugin-schemas follows plugin DIRECTORIES — so on a host without that directory the table is in the database and not in the schema, and `db:push` proposes dropping it. on alpha that is eight device credentials. so this plan answers it from a survey rather than a guess. alpha has the plugin and 8 rows; the other six have no plugin and an empty leftover table; zero hosts run officer-marketplace, which retires the `tags` column question entirely. the orphan table is dropped EXPLICITLY, guarded by a count, and skipped on alpha — never as a db:push side effect. step 6 is new and would have been missed: officer-opencode has to restart or none of the skills, agent-identity or credential work in this release does anything. ALSO THE SIGN-OUT. a single 401 from any endpoint wiped the token from both storages and reloaded — which signs the owner out of every tab and keeps them there, since nothing is left to recover from. that is the right response to a revoked session and far too violent a response to one endpoint answering 401 for its own reasons. it now confirms against /auth/me first, and treats an unreachable server as emphatically not a revocation: rebooting the box must not end the session. reported mid-demo, with nothing in the logs and a freshly signed token working against every route the app loads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -206,6 +206,45 @@ const handleRevokedSession = (url: string) => {
|
||||
if (revocationHandled) return;
|
||||
revocationHandled = true;
|
||||
|
||||
/*
|
||||
Guard 2b: CONFIRM the session is dead before destroying it.
|
||||
|
||||
A 401 from any one endpoint used to be enough to wipe the token out of both storages and reload —
|
||||
which signs the owner out of every tab, and keeps them signed out, because there is then no token
|
||||
anywhere to recover from. That is the right response to a revoked session and far too violent a
|
||||
response to one endpoint answering 401 for its own reasons.
|
||||
|
||||
Reported 2026-08-27, mid-demo: every refresh and every new tab came back signed out, with nothing in
|
||||
the logs and a freshly signed token working fine against every route the app loads. One transient
|
||||
401 is indistinguishable from a revocation at the call site — so ask the one endpoint whose whole
|
||||
job is to answer it.
|
||||
|
||||
`/auth/me` is exempt from this handler (the `/auth/` guard above), so this cannot recurse.
|
||||
*/
|
||||
void (async () => {
|
||||
try {
|
||||
// Derived from the URL that just failed, because the base is passed per call rather than held in
|
||||
// config — a relative guess would be wrong on any host that mounts the API elsewhere.
|
||||
const probe = /\/api\//.test(url) ? url.replace(/\/api\/.*$/, '/api/auth/me') : '/api/auth/me';
|
||||
const res = await fetch(probe, { headers: { Authorization: `Bearer ${theToken}` } });
|
||||
// Still good: that 401 belonged to one endpoint, not to the session. Leave everything alone and
|
||||
// let the next revocation try again.
|
||||
if (res.ok) {
|
||||
revocationHandled = false;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// The server is unreachable — a restart, a dropped network. Emphatically not a revocation, and
|
||||
// wiping the session because the box was rebooting is the failure this guard exists to stop.
|
||||
revocationHandled = false;
|
||||
return;
|
||||
}
|
||||
destroySession();
|
||||
})();
|
||||
};
|
||||
|
||||
/** Actually end it: forget the token everywhere a load could read it back, then reload once. */
|
||||
const destroySession = () => {
|
||||
theToken = null;
|
||||
// Every key `createClient` reads, or the next load signs straight back in with the dead token.
|
||||
for (const key of ['PERTENTO_EDITOR_AUTH_TOKEN', 'BEARER_TOKEN']) {
|
||||
|
||||
Reference in New Issue
Block a user