@camstack/server 1.2.246 → 1.2.248

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ exports.shouldRedirectToLogin = shouldRedirectToLogin;
8
8
  exports.loginRedirectUrl = loginRedirectUrl;
9
9
  exports.isEmbedRedirectTarget = isEmbedRedirectTarget;
10
10
  exports.isSessionGradeJwtPayload = isSessionGradeJwtPayload;
11
+ exports.sessionCookieIsSecure = sessionCookieIsSecure;
11
12
  /** Browser session cookie carrying the hub JWT. Set by POST /api/auth/session
12
13
  * after a tRPC login; read by the addon-route catch-all for `authenticated`
13
14
  * routes hit by a plain browser navigation. */
@@ -105,3 +106,43 @@ function isSessionGradeJwtPayload(payload) {
105
106
  const p = payload;
106
107
  return p.kind === undefined && typeof p.userId === 'string' && typeof p.isAdmin === 'boolean';
107
108
  }
109
+ /**
110
+ * Should the session cookie carry `Secure`, for a request that may have crossed
111
+ * a reverse proxy?
112
+ *
113
+ * The hub decides this from its own leg of the connection, and behind a proxy
114
+ * that leg is the wrong one. Home Assistant's relay reaches the hub over HTTPS
115
+ * while the browser reached Home Assistant over plain HTTP on the LAN, so the
116
+ * hub marked the cookie `Secure` and the browser silently DISCARDED it: the
117
+ * login succeeded, the session existed, and every `<img>` that can only
118
+ * authenticate by cookie came back 401 while the rest of the app — which
119
+ * attaches its bearer token explicitly — worked. That is why the admin UI
120
+ * showed no snapshots inside Home Assistant and everything else looked fine.
121
+ *
122
+ * The relay already SENDS `X-Forwarded-Proto` with the browser's own scheme,
123
+ * next to the prefix header the admin UI's `<base>` depends on. Only this side
124
+ * was missing: the hub read its own socket and never the declaration.
125
+ *
126
+ * Two guards, because `X-Forwarded-Proto` is a client-supplied string:
127
+ *
128
+ * - it may only ever DOWNGRADE. A request that arrived here over plain HTTP
129
+ * can never be talked into claiming HTTPS, so the header cannot be used to
130
+ * make a cookie look safer than its transport.
131
+ * - it is read ONLY when the request also carries the relay's prefix header,
132
+ * which is set by the proxy and by nothing else on a direct browser hit. A
133
+ * direct caller inventing the proto header alone changes nothing.
134
+ */
135
+ function sessionCookieIsSecure(requestProtocol, headers, forwardedPrefixHeader) {
136
+ const secureLeg = requestProtocol === 'https';
137
+ if (!secureLeg)
138
+ return false;
139
+ if (headers[forwardedPrefixHeader] === undefined)
140
+ return true;
141
+ const raw = headers['x-forwarded-proto'];
142
+ const proto = Array.isArray(raw) ? raw[0] : raw;
143
+ if (proto === undefined)
144
+ return true;
145
+ // A comma list means several hops; the FIRST is the browser's own scheme.
146
+ const browserScheme = proto.split(',')[0]?.trim().toLowerCase();
147
+ return browserScheme !== 'http';
148
+ }
package/dist/main.js CHANGED
@@ -894,13 +894,17 @@ async function bootstrap() {
894
894
  catch {
895
895
  return reply.status(401).send({ error: 'invalid token' });
896
896
  }
897
- const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, { secure: request.protocol === 'https' });
897
+ const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
898
+ secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
899
+ });
898
900
  reply.setCookie(c.name, c.value, c.options);
899
901
  return reply.send({ ok: true });
900
902
  });
901
903
  // DELETE /api/auth/session — clear the cookie on logout.
902
904
  fastify.delete('/api/auth/session', async (request, reply) => {
903
- const c = (0, session_cookie_js_1.clearSessionCookie)({ secure: request.protocol === 'https' });
905
+ const c = (0, session_cookie_js_1.clearSessionCookie)({
906
+ secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
907
+ });
904
908
  reply.setCookie(c.name, c.value, c.options);
905
909
  return reply.send({ ok: true });
906
910
  });
@@ -930,7 +934,9 @@ async function bootstrap() {
930
934
  catch {
931
935
  return reply.status(401).send({ error: 'invalid token' });
932
936
  }
933
- const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, { secure: request.protocol === 'https' });
937
+ const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
938
+ secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
939
+ });
934
940
  reply.setCookie(c.name, c.value, c.options);
935
941
  return reply.redirect(next);
936
942
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.246",
3
+ "version": "1.2.248",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",