mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-01 01:33:32 +02:00
fix(claude_desktop): sign-in persistence broke again — safeStorage patcher didn't handle bundles without a use-strict directive (#2983)
* fix(claude_desktop): patch safeStorage on bundles without a use-strict directive The v1.37 safeStorage patcher only knew how to inject its plaintext-encryption opt-in after a leading "use strict" directive in Claude Desktop's main bundle, and refused to patch anything else. Confirmed live: Claude Desktop 1.30096.1's main bundle no longer opens with that directive (bare IIFE instead), so the patch has been silently refusing to run on every boot and sessions stopped persisting across restarts again, with the same "Encryption not available" warning documented in SIGN_IN.md before v1.37. applyPatch() now falls back to prepending the opt-in as the bundle's first statement when no directive is found, after skipping any leading BOM, hashbang, or banner comment so a directive hidden behind a comment is still protected rather than pushed out of position zero. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(claude_desktop): scan the full directive prologue, not just line 0 Addresses two Codex review findings on PR #2983: 1. skipPrologue()'s //-comment scan only recognized "\n" as a line terminator. A comment ending in CR-only or U+2028/U+2029 (all valid ECMAScript LineTerminators) made it swallow the rest of the file as "still the comment", landing the patch after the bundle's last statement instead of before it. Reproduced with `// banner\r"use strict";(function(){})();`. 2. applyPatch() only checked whether the very first statement was literally "use strict". A directive prologue can hold more than one string-literal statement, and "use strict" only has to appear somewhere in it, not first; prepending ahead of an earlier directive pushed the whole prologue out of first-statement position and silently dropped strict mode. Reproduced with `"use custom";"use strict";(...)`. Replaced the single-directive check with scanDirectivePrologue(), which walks every leading string-literal-only statement and inserts right after the full prologue (or at the same position when there is none). skipPrologue/applyPatch split into skipBomAndHashbang + skipWhitespaceAndComments + scanDirectivePrologue accordingly. Verified: both findings reproduced against the pre-fix code and no longer occur; 13-case regression suite covering the original edge cases plus both findings all pass; re-run against the live production app.asar still patches successfully and idempotently. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,23 @@
|
||||
|
||||
## 07308543.1 (17-08-2026)
|
||||
- Fix the "For your security, sign in again" prompt recurring on every restart again. The v1.37
|
||||
`safeStorage` patch (`86-claude_safestorage.sh` / `claude-safestorage-patch.js`) only knew how
|
||||
to inject its opt-in after a leading `"use strict"` directive in the app's main bundle, and
|
||||
refused to patch anything else. Confirmed live on the running add-on (Claude Desktop
|
||||
1.30096.1): the shipped main bundle (`.vite/build/index.pre.js`) no longer opens with a
|
||||
`"use strict"` directive at all — it now opens directly with a bare IIFE — so the patcher has
|
||||
been silently refusing to patch on every boot, `safeStorage.isEncryptionAvailable()` stayed
|
||||
`false`, and the app's own log kept showing `Encryption not available, returning empty env
|
||||
vars` exactly as before v1.37. `applyPatch()` now falls back to inserting the opt-in as the
|
||||
bundle's first real statement when no directive is present, skipping past any leading BOM,
|
||||
hashbang, or banner comment first so a directive hidden behind a comment is still found and
|
||||
protected rather than pushed out of position zero. Verified by copying the live production
|
||||
`app.asar` and running the patcher against it directly: the previously-refused bundle now
|
||||
patches successfully, the marker lands correctly, a second run reports "Already patched", and
|
||||
targeted unit tests cover the bare-IIFE, comment-hidden-directive, hashbang, and
|
||||
unterminated-comment cases.
|
||||
- One-time step after upgrading, same as v1.37: the previously-stored session is already stale,
|
||||
so complete one sign-in from a computer; it then persists across restarts.
|
||||
## 07308545 (2026-08-15)
|
||||
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
|
||||
- Upstream tag : v3.2.2+claude1.30096.1
|
||||
|
||||
@@ -13,8 +13,10 @@ streamed desktop.
|
||||
offline until a fresh sign-in was done from a computer). v1.35 switched to
|
||||
`--password-store=basic` plus a cont-init script that re-syncs the persistent openbox
|
||||
`autostart` from the image on every boot — **but that flag alone does nothing**, and the bug
|
||||
survived it untouched. Actually fixed in v1.37, which adds the application-side opt-in the
|
||||
`basic` backend requires; see "Why v1.35 did not work" below.
|
||||
survived it untouched. v1.37 added the application-side opt-in the `basic` backend requires;
|
||||
see "Why v1.35 did not work" below. That patcher then regressed silently when upstream's
|
||||
bundle output changed shape — fixed again in 07308543.1; see "Why v1.37 stopped working"
|
||||
below.
|
||||
- **Planned only:** Problem A (in-desktop browser for OAuth) is intentionally not implemented.
|
||||
The image ships no browser; complete the login with the user-side workaround below.
|
||||
|
||||
@@ -170,10 +172,40 @@ The third row is the one that matters: it is the restart survival this add-on ne
|
||||
reaches upgrades, not just fresh installs.
|
||||
4. `gnome-keyring` stays out of the Dockerfile.
|
||||
|
||||
### Why v1.37 stopped working
|
||||
|
||||
`claude-safestorage-patch.js` only knew how to inject its opt-in *after* a leading
|
||||
`"use strict"` directive in the app's main bundle, and refused to patch (leaving the app
|
||||
unpatched and the session un-persisted) if that directive wasn't there. Confirmed live on the
|
||||
running add-on: Claude Desktop 1.30096.1's main bundle (`.vite/build/index.pre.js`) no longer
|
||||
opens with a `"use strict"` directive — it now opens directly with a bare IIFE
|
||||
(`(function(){try{var e=typeof window...`). Upstream's build output changed shape at some point
|
||||
after v1.37 shipped, the patcher's one injection point stopped existing, and it had been
|
||||
silently refusing to patch on every boot since — the app's `main.log` kept showing exactly the
|
||||
same `Encryption not available, returning empty env vars` warning documented above, and the
|
||||
session went back to not surviving restarts.
|
||||
|
||||
`applyPatch()` now falls back to inserting the opt-in as the bundle's first real statement when
|
||||
no `"use strict"` directive is found, rather than refusing outright. It skips past any leading
|
||||
BOM, hashbang, or banner comment first (`skipPrologue()`), so a directive hidden behind a
|
||||
comment is still found and protected instead of being pushed out of the first-statement
|
||||
position by a naive prepend — Vite/esbuild banners commonly put a license comment ahead of the
|
||||
directive. A bundle with no directive at all has nothing to protect, so prepending the opt-in
|
||||
there is unconditionally safe: the injected code is a complete `try{}catch(e){}` statement, and
|
||||
a statement can never merge with what follows it via ASI the way a bare expression could.
|
||||
|
||||
Verified by copying the live production `app.asar` and running the patcher against it directly
|
||||
(outside the container's boot sequence): the previously-refused bundle now patches
|
||||
successfully, the marker lands at the front of the main entry, a second run correctly reports
|
||||
"Already patched" (idempotent), and unit tests cover the bare-IIFE, comment-hidden-directive,
|
||||
hashbang, and unterminated-comment cases.
|
||||
|
||||
### One-time step after upgrading
|
||||
The previously-stored session is already stale. Complete **one** sign-in from a computer
|
||||
(mobile still can't finish the OAuth flow itself, per Problem A) — the session then persists
|
||||
normally and dispatch stays online regardless of which device connects first afterward.
|
||||
normally and dispatch stays online regardless of which device connects first afterward. This
|
||||
applies again after the 07308543.1 fix above, since the affected sessions were never persisted
|
||||
in the first place.
|
||||
|
||||
---
|
||||
|
||||
@@ -185,7 +217,10 @@ normally and dispatch stays online regardless of which device connects first aft
|
||||
- `claude_desktop/rootfs/etc/cont-init.d/86-claude_safestorage.sh` and
|
||||
`claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js` — new in v1.37; the
|
||||
app-side `safeStorage` opt-in that makes `--password-store=basic` actually take effect.
|
||||
`claude-safestorage-patch.js` updated again in 07308543.1 to also patch bundles with no
|
||||
leading `"use strict"` directive, and to look past leading comments/hashbang when deciding
|
||||
whether one is present.
|
||||
- `claude_desktop/Dockerfile` — corrected stale comment (gnome-keyring is not installed).
|
||||
- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35, then v1.37.
|
||||
- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35, then v1.37, then 07308543.1.
|
||||
|
||||
Problem A (in-desktop browser for OAuth) remains planned-only; not touched by this change.
|
||||
|
||||
@@ -136,5 +136,5 @@ schema:
|
||||
slug: claude_desktop
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "07308545"
|
||||
version: "07308545.1"
|
||||
video: true
|
||||
|
||||
@@ -128,24 +128,98 @@ function integrityOf(buf, blockSize) {
|
||||
return { algorithm: 'SHA256', hash: sha256(buf), blockSize, blocks };
|
||||
}
|
||||
|
||||
/* Insert the opt-in after the bundle's leading "use strict" directive. It must go *after* it: a
|
||||
* directive prologue only takes effect as the very first statement, so prepending would silently
|
||||
* drop the whole main process out of strict mode.
|
||||
// Any of the four ECMAScript LineTerminator code points — not just "\n". A //-comment or an ASI
|
||||
// boundary ends at the first of these, and using a bare "\n" search for that would let a CR- or
|
||||
// U+2028/U+2029-terminated line swallow real code as "still the comment/still on this line" and
|
||||
// misplace the insertion point deep inside the bundle instead of before it.
|
||||
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
|
||||
|
||||
/* Skip a leading BOM and hashbang line. Only meaningful at byte 0 — called once, before any
|
||||
* directive scanning. */
|
||||
function skipBomAndHashbang(source) {
|
||||
let i = source.charCodeAt(0) === 0xfeff ? 1 : 0; // BOM
|
||||
if (source.startsWith('#!', i)) {
|
||||
const m = LINE_TERMINATOR.exec(source.slice(i));
|
||||
i += m ? m.index + 1 : source.length - i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/* Skip whitespace and comments starting at i. Returns the next index, or -1 for an unterminated
|
||||
* block comment (caller refuses rather than guesses). */
|
||||
function skipWhitespaceAndComments(source, i) {
|
||||
for (;;) {
|
||||
const rest = source.slice(i);
|
||||
const ws = /^\s+/.exec(rest);
|
||||
if (ws) {
|
||||
i += ws[0].length;
|
||||
continue;
|
||||
}
|
||||
if (rest.startsWith('//')) {
|
||||
const m = LINE_TERMINATOR.exec(rest);
|
||||
i += m ? m.index + 1 : rest.length;
|
||||
continue;
|
||||
}
|
||||
if (rest.startsWith('/*')) {
|
||||
const end = rest.indexOf('*/');
|
||||
if (end === -1) return -1;
|
||||
i += end + 2;
|
||||
continue;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// A single-line string literal: no raw line terminator in its content (a real one would need an
|
||||
// escaped line continuation, which this deliberately doesn't special-case — failing to match
|
||||
// just means the prologue scan below stops there, which is always safe, see applyPatch).
|
||||
const STRING_LITERAL = /^(['"])(?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*\1/;
|
||||
|
||||
/* Scan the bundle's full leading directive prologue: every consecutive ExpressionStatement made
|
||||
* of nothing but a string literal, per how ECMAScript directives actually work. A directive
|
||||
* prologue can hold more than one entry, and "use strict" only has to appear *somewhere* in it,
|
||||
* not first — so this treats every leading directive as needing protection, not just one
|
||||
* specifically named "use strict". Returns the index right after the full prologue (which is
|
||||
* also correct as "no prologue, insert here" when there wasn't one), or -1 when a leading string
|
||||
* literal isn't cleanly terminated as its own statement — ambiguous whether it's a directive at
|
||||
* all, refused rather than guessed at. */
|
||||
function scanDirectivePrologue(source, start) {
|
||||
let i = start;
|
||||
for (;;) {
|
||||
const next = skipWhitespaceAndComments(source, i);
|
||||
if (next === -1) return -1;
|
||||
const rest = source.slice(next);
|
||||
const m = STRING_LITERAL.exec(rest);
|
||||
if (!m) return next; // not a directive; prologue (possibly empty) ends here
|
||||
const after = rest.slice(m[0].length);
|
||||
if (after[0] === ';') {
|
||||
i = next + m[0].length + 1;
|
||||
} else if (after === '' || LINE_TERMINATOR.test(after[0])) {
|
||||
i = next + m[0].length;
|
||||
} else {
|
||||
return -1; // "use strict" + x and friends: not unambiguously a directive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert the opt-in right after the bundle's full leading directive prologue (BOM/hashbang, then
|
||||
* any run of string-literal-only statements — "use strict" among them if present). It must go
|
||||
* *after* the whole prologue, not just after the first entry: a directive prologue only takes
|
||||
* effect when its members are the very first statements, so inserting between two of them, or
|
||||
* ahead of all of them, would silently drop the file out of strict mode just as surely as
|
||||
* inserting ahead of a lone "use strict" would.
|
||||
*
|
||||
* Returns null — meaning "refuse to patch" — for anything that is not unambiguously a directive.
|
||||
* `"use strict" + x` is an expression, not a directive, and injecting into it would produce a
|
||||
* syntax error, so the directive is only accepted when it is terminated by its own semicolon, a
|
||||
* line break, or end of input. */
|
||||
* When there is no prologue at all (observed from Claude Desktop 1.30096.1 onward, whose main
|
||||
* entry opens with a bare IIFE instead), there is nothing to preserve: PATCH lands at the same
|
||||
* position anyway, as the file's first real statement. A `try{}catch(e){}` statement can never
|
||||
* merge with whatever follows via ASI — unlike a bare expression, a statement is not a valid
|
||||
* left-hand side for anything a following token could continue — so this is unconditionally
|
||||
* safe once placed after any banner comment / hashbang / directive prologue. */
|
||||
function applyPatch(source) {
|
||||
const m = /^\s*(['"])use strict\1(;?)/.exec(source);
|
||||
if (!m) return null;
|
||||
const rest = source.slice(m[0].length);
|
||||
const terminated = m[2] === ';' || rest === '' || /^[\r\n]/.test(rest);
|
||||
if (!terminated) return null;
|
||||
// Supply the terminator when the directive relied on ASI; without it the injected code would
|
||||
// continue the string-literal expression instead of following it.
|
||||
const sep = m[2] === ';' ? '' : ';';
|
||||
return source.slice(0, m[0].length) + sep + PATCH + rest;
|
||||
const start = skipBomAndHashbang(source);
|
||||
const end = scanDirectivePrologue(source, start);
|
||||
if (end === -1) return null;
|
||||
return source.slice(0, end) + PATCH + source.slice(end);
|
||||
}
|
||||
|
||||
function writeAll(fd, buf) {
|
||||
@@ -203,7 +277,7 @@ function main() {
|
||||
|
||||
const patchedSource = applyPatch(original);
|
||||
if (patchedSource === null) {
|
||||
fail(`${mainRel} does not begin with a recognized "use strict" directive; refusing to patch`);
|
||||
fail(`${mainRel} opens with an ambiguous "use strict"-like string literal; refusing to patch`);
|
||||
}
|
||||
const patched = Buffer.from(patchedSource, 'utf8');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user