fix(mana/web/news): use getValidToken + guard prefs against locked vault

After the previous round of fixes, two issues remained:

1. Feed fetch returned 401 against `mana-api.mana.how`. The new
   `authHeader()` helper called `authStore.getAccessToken()`, which
   just reads `@auth/appToken` from localStorage and is happy to return
   null/stale. The unified sync engine in `sync.ts` uses
   `authStore.getValidToken()`, which routes through the tokenManager
   and refreshes if needed. Switched the news client to the same.

2. `Cannot read properties of undefined (reading 'emoji')` from
   `TOPIC_LABELS[topic]`. When the vault is briefly locked at boot,
   `decryptRecord` deliberately leaves the encrypted blob string in
   place — so `local.selectedTopics` can be a string. The `?? []`
   fallback in `toPreferences` doesn't catch it, and `{#each
   prefs.selectedTopics}` iterates the blob char-by-char. Force the
   three array fields (and the two map fields) back to their expected
   shapes with `Array.isArray` / object checks.
This commit is contained in:
Till JS 2026-04-09 18:52:51 +02:00
parent 5520f1385e
commit 3b035e930f
2 changed files with 18 additions and 6 deletions

View file

@ -25,7 +25,11 @@ import { authStore } from '$lib/stores/auth.svelte';
import { getManaApiUrl } from '$lib/api/config';
async function authHeader(): Promise<Record<string, string>> {
const token = await authStore.getAccessToken();
// getValidToken (not getAccessToken) — runs the token through the
// tokenManager so it refreshes if expired. getAccessToken just reads
// localStorage and returns null/stale, which is what made the first
// pass at this fix still 401. sync.ts uses the same getValidToken.
const token = await authStore.getValidToken();
return token ? { Authorization: `Bearer ${token}` } : {};
}

View file

@ -69,13 +69,21 @@ export function toCategory(local: LocalCategory): Category {
}
export function toPreferences(local: LocalPreferences): Preferences {
// Force the array fields back to arrays even if decryption left an
// encrypted blob string in place (vault locked at boot). Without this
// guard `{#each prefs.selectedTopics}` iterates the encrypted string
// char-by-char and crashes `TOPIC_LABELS[topic].emoji` on render.
return {
id: local.id,
selectedTopics: local.selectedTopics ?? [],
blockedSources: local.blockedSources ?? [],
preferredLanguages: local.preferredLanguages ?? ['de', 'en'],
topicWeights: local.topicWeights ?? {},
sourceWeights: local.sourceWeights ?? {},
selectedTopics: Array.isArray(local.selectedTopics) ? local.selectedTopics : [],
blockedSources: Array.isArray(local.blockedSources) ? local.blockedSources : [],
preferredLanguages: Array.isArray(local.preferredLanguages)
? local.preferredLanguages
: ['de', 'en'],
topicWeights:
local.topicWeights && typeof local.topicWeights === 'object' ? local.topicWeights : {},
sourceWeights:
local.sourceWeights && typeof local.sourceWeights === 'object' ? local.sourceWeights : {},
onboardingCompleted: local.onboardingCompleted ?? false,
};
}