chore(ws): verbose auth logs to triage Disconnected reports
Browse filesAdd structured logging on both ends of the WS handshake so a
"Disconnected" label in the editor top bar can be pinpointed
without guessing:
- Upgrade handler logs the cookie header size and whether
extractToken found our session cookie. This catches the case
where the browser doesn't ship cookies on the WS upgrade
(some HF Space gating + private/org setups strip them on
upgrade even when plain HTTP requests send them fine).
- onAuthenticate logs which source supplied the token (the
client subprotocol or the server-side cookie fallback), the
resolved user, the configured Space owner, and any
accessIssue surfaced by resolveUser. Errors thrown to
Hocuspocus now also include the username + accessIssue so the
reason is visible in the rejected-connection log line, not
just a generic "Unauthorized".
Logs intentionally avoid printing the token itself - they only
expose its length and origin, matching the existing privacy
posture of the auth pipeline.
Co-authored-by: Cursor <cursoragent@cursor.com>
- backend/src/create-app.ts +52 -2
|
@@ -306,11 +306,51 @@ export function createApp() {
|
|
| 306 |
if (!oauthEnabled) return;
|
| 307 |
|
| 308 |
const { resolveUser } = await import("./auth.js");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
const authToken = token || context?.token;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
const user = await resolveUser(authToken);
|
| 311 |
-
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
if (authToken) setUserToken(authToken);
|
| 315 |
return { user };
|
| 316 |
},
|
|
@@ -389,6 +429,16 @@ export function createApp() {
|
|
| 389 |
});
|
| 390 |
}
|
| 391 |
const token = extractToken(req.headers.cookie);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
hocuspocus.handleConnection(ws, req, { token });
|
| 393 |
});
|
| 394 |
} else {
|
|
|
|
| 306 |
if (!oauthEnabled) return;
|
| 307 |
|
| 308 |
const { resolveUser } = await import("./auth.js");
|
| 309 |
+
// Two-source pattern: the HocuspocusProvider client sends `token`
|
| 310 |
+
// via the WS sub-protocol, but our cookie is httpOnly so the
|
| 311 |
+
// client can't read it and sends "" instead. Fall back to the
|
| 312 |
+
// cookie the upgrade handler stuffed into context.token.
|
| 313 |
const authToken = token || context?.token;
|
| 314 |
+
|
| 315 |
+
// Surface enough info in the Space logs to triage "Disconnected"
|
| 316 |
+
// reports without leaking the token itself. We log:
|
| 317 |
+
// - whether each source produced a token (just truthiness)
|
| 318 |
+
// - the resolved user (or null) + accessIssue when present
|
| 319 |
+
// - the SPACE_ID owner being checked, to spot org mismatches
|
| 320 |
+
const tokenSource = token
|
| 321 |
+
? "client"
|
| 322 |
+
: context?.token
|
| 323 |
+
? "cookie"
|
| 324 |
+
: "none";
|
| 325 |
+
const tokenLen = authToken ? authToken.length : 0;
|
| 326 |
+
|
| 327 |
const user = await resolveUser(authToken);
|
| 328 |
+
const spaceOwner = (process.env.SPACE_ID || "").split("/")[0] || "(none)";
|
| 329 |
+
|
| 330 |
+
if (!user) {
|
| 331 |
+
console.warn(
|
| 332 |
+
`[ws-auth] reject: no user resolved` +
|
| 333 |
+
` source=${tokenSource} tokenLen=${tokenLen}` +
|
| 334 |
+
` spaceOwner=${spaceOwner}`,
|
| 335 |
+
);
|
| 336 |
+
throw new Error("Unauthorized: invalid or missing HF token");
|
| 337 |
}
|
| 338 |
+
if (!user.canEdit) {
|
| 339 |
+
console.warn(
|
| 340 |
+
`[ws-auth] reject: ${user.name} can't write to ${spaceOwner}` +
|
| 341 |
+
` issue=${user.accessIssue ?? "unknown"}` +
|
| 342 |
+
` source=${tokenSource}`,
|
| 343 |
+
);
|
| 344 |
+
throw new Error(
|
| 345 |
+
`Unauthorized: ${user.name} has no write access to ${spaceOwner}` +
|
| 346 |
+
(user.accessIssue ? ` (${user.accessIssue})` : ""),
|
| 347 |
+
);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
console.log(
|
| 351 |
+
`[ws-auth] accept user=${user.name} spaceOwner=${spaceOwner}` +
|
| 352 |
+
` source=${tokenSource}`,
|
| 353 |
+
);
|
| 354 |
if (authToken) setUserToken(authToken);
|
| 355 |
return { user };
|
| 356 |
},
|
|
|
|
| 429 |
});
|
| 430 |
}
|
| 431 |
const token = extractToken(req.headers.cookie);
|
| 432 |
+
// Diagnostic for "Disconnected" reports: confirms whether the
|
| 433 |
+
// browser actually attached our session cookie to the WS
|
| 434 |
+
// upgrade. On HF Spaces, some gating setups occasionally
|
| 435 |
+
// strip cookies on WS upgrades even when they're sent for
|
| 436 |
+
// plain HTTP, which manifests as a working /editor route
|
| 437 |
+
// but a permanently-failing WS auth.
|
| 438 |
+
const cookieHeader = req.headers.cookie || "";
|
| 439 |
+
console.log(
|
| 440 |
+
`[ws] upgrade cookies=${cookieHeader.length}B hasToken=${Boolean(token)}`,
|
| 441 |
+
);
|
| 442 |
hocuspocus.handleConnection(ws, req, { token });
|
| 443 |
});
|
| 444 |
} else {
|