fix v1.44.3: signed content paths — plans and PDFs load again (B1 regression)

The v1.43.0 auth fix closed the hole but left the DISPLAY path
unauthenticated: HA authenticates by a Bearer header or an authSig
signed path, and an <image href> / <a href> sends neither, so plan
backgrounds and manual links returned 401. Reproduced live before the
fix (fetch 401, Image onerror).

- new WS houseplan/content/sign mints async_sign_path urls (24 h,
  bound to the connection's refresh token, only for our own endpoint)
- the card resolves display urls through _display(): signed when known,
  requests a batched signature otherwise, re-renders when it lands, and
  drops all signatures every 12 h so long-lived wall tablets stay valid
- houseplan-space-card signs its background too
- backend test asserts the unsigned url is refused and the signed one
  returns the bytes WITHOUT an Authorization header
This commit is contained in:
Matysh
2026-07-27 14:08:29 +03:00
parent c0653dfc73
commit 0467cee98a
7 changed files with 211 additions and 54 deletions
+23
View File
@@ -109,6 +109,8 @@ class HouseplanSpaceCard extends LitElement {
public getCardSize(): number {
const models = spaceModels(this._snap?.config || null);
// B1 follow-up: the plan <image> needs a signed url in a browser session
for (const m of models) if (m.bg?.href) this._signBg(m.bg);
const sp = models.find((s) => s.id === this._config?.space);
if (sp) {
const ratio = sp.vb[3] / sp.vb[2]; // h/w
@@ -121,6 +123,27 @@ class HouseplanSpaceCard extends LitElement {
return html`<ha-card><div class="hp-static-error">${msg}</div></ha-card>`;
}
private _signedBg: Record<string, string> = {};
private _signBgPending = new Set<string>();
/** Ask the backend for a signed content url and swap it in when it arrives. */
private _signBg(bg: { href: string }): void {
const raw = bg.href.split('?authSig=')[0];
if (!raw.startsWith('/api/houseplan/content/')) return;
if (this._signedBg[raw]) { bg.href = this._signedBg[raw]; return; }
if (this._signBgPending.has(raw) || !this.hass?.callWS) return;
this._signBgPending.add(raw);
this.hass
.callWS({ type: 'houseplan/content/sign', paths: [raw] })
.then((r: any) => {
const url = r?.urls?.[raw];
if (!url) return;
this._signedBg = { ...this._signedBg, [raw]: url };
this.requestUpdate();
})
.catch(() => undefined);
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) return nothing;
const cfg = this._snap?.config;