feat: expose PWA, app identity, and extension directory keys in JSON config #312

This commit is contained in:
Linus Rath
2026-05-21 23:35:58 +02:00
parent 9b22ef810e
commit fc5f6f43d6
58 changed files with 159 additions and 130 deletions
+2 -2
View File
@@ -1256,7 +1256,7 @@ export const useAuthStore = create<AuthState>()(
return;
}
// Orphan-cookie adoption when no accounts are registered but a
// Orphan-cookie adoption - when no accounts are registered but a
// basic-auth session cookie is present (set by /api/auth/impersonate
// or by another server-side hand-off), promote it into the account
// registry so the normal restoration path picks it up. Without this
@@ -1664,5 +1664,5 @@ export const useAuthStore = create<AuthState>()(
);
// Expose getClientForAccount to the calendar/contact stores via a small
// shared registry see [[stores/client-registry]] for rationale.
// shared registry - see [[stores/client-registry]] for rationale.
setClientLookup((accountId) => useAuthStore.getState().getClientForAccount(accountId));
+7 -7
View File
@@ -15,7 +15,7 @@ import { getClientByLocalAccountId } from './client-registry';
/**
* When the Pro shell aggregates calendars/events from every connected
* account, the entity carries a `localAccountId` pointing back to the
* owning JMAP client. Mutations need to use *that* client the active
* owning JMAP client. Mutations need to use *that* client - the active
* client (passed in by the page) could be on a different server entirely.
* Falls back to the active client when `localAccountId` is unset or no
* matching client is registered.
@@ -75,7 +75,7 @@ function prefixCalendarsWithLocalAccount(
return calendars.map((cal) => ({ ...cal, localAccountId }));
}
const prefix = buildCrossAccountIdPrefix(localAccountId);
// Preserve each calendar's original `isShared` flag it distinguishes
// Preserve each calendar's original `isShared` flag - it distinguishes
// the user's own calendars on the other account from calendars shared
// *into* that account by yet another user. The sidebar uses this split
// to render "My Calendars" vs "Shared" sub-sections per account.
@@ -186,7 +186,7 @@ export interface ICalSubscription {
url: string;
calendarId: string;
// The JMAP account this subscription belongs to. Optional for back-
// compat with subs persisted before multi-account scoping landed
// compat with subs persisted before multi-account scoping landed -
// legacy entries with no accountId are shown only in whichever account
// the user has active (treated as floating). New subs always set it.
accountId?: string;
@@ -910,7 +910,7 @@ export const useCalendarStore = create<CalendarStore>()(
if (calendarEvents.length === 0) break;
// Separate events that live ONLY in this calendar (delete) from
// events also linked to other calendars (unlink only don't
// events also linked to other calendars (unlink only - don't
// cascade-delete the user's copy elsewhere).
const idsToDelete: string[] = [];
const eventsToUnlink: Array<{ id: string; calendarIds: Record<string, boolean> }> = [];
@@ -1008,7 +1008,7 @@ export const useCalendarStore = create<CalendarStore>()(
icalSubscriptions: [...state.icalSubscriptions, subscription],
}));
// Initial fetch roll back the calendar create if it fails so we
// Initial fetch - roll back the calendar create if it fails so we
// don't leave a phantom calendar around after a bad URL / 404 / etc.
await get().refreshICalSubscription(client, subscription.id);
@@ -1097,7 +1097,7 @@ export const useCalendarStore = create<CalendarStore>()(
if (!sub) return;
// Skip if the subscription is scoped to a different JMAP account
// than the one this client is talking to otherwise we'd create
// than the one this client is talking to - otherwise we'd create
// events in the wrong account / against a missing calendar.
if (sub.accountId && sub.accountId !== client.getAccountId()) {
debug.warn('calendar', 'Skipping subscription refresh: account mismatch', { sub: sub.name });
@@ -1228,7 +1228,7 @@ export const useCalendarStore = create<CalendarStore>()(
clearState: () => {
// Preserve iCal subscriptions across the account-switch teardown.
// They're now scoped per-account via sub.accountId wiping them
// They're now scoped per-account via sub.accountId - wiping them
// here would lose them from localStorage on every switch.
const preservedSubs = get().icalSubscriptions;
set({
+1 -1
View File
@@ -3,7 +3,7 @@ import type { IJMAPClient } from '@/lib/jmap/client-interface';
/**
* Tiny indirection used by the calendar and contact stores to look up a
* JMAP client by local account ID without importing `auth-store` directly
* that would form a top-level cycle (auth-store already imports the
* - that would form a top-level cycle (auth-store already imports the
* feature stores to bootstrap them after login).
*
* `auth-store` registers its `getClientForAccount` on module init via
+3 -3
View File
@@ -23,7 +23,7 @@ interface EmailStore {
accountMailboxes: Record<string, Mailbox[]>;
/**
* When set, the mail view is reading from this account instead of the
* global active one. `null` means "use the global active account" i.e.
* global active one. `null` means "use the global active account" - i.e.
* the standard single-account behavior. Selecting a folder under a
* non-active account in the Pro sidebar updates this without changing
* `useAuthStore.activeAccountId`.
@@ -249,7 +249,7 @@ function resolveActionMailboxes(): Mailbox[] {
* Builds the `UnifiedAccountClient[]` list used by every unified fan-out
* action (browse, load-more, search). Each entry has a JMAP client plus a
* fresh mailbox list so the helpers can resolve the role mailbox per account.
* Accounts whose mailbox fetch fails are skipped the unified result will
* Accounts whose mailbox fetch fails are skipped - the unified result will
* surface that in its per-account error map.
*/
async function buildUnifiedAccountClients(): Promise<UnifiedAccountClient[]> {
@@ -1162,7 +1162,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
}));
// Refresh mailbox folder lists/counters for every account we touched.
// Background-only so the move feels instant counters will catch up.
// Background-only so the move feels instant - counters will catch up.
const activeAccountId = useAuthStore.getState().activeAccountId;
const touched = new Set<string>([destAccountId, ...emailIdsBySource.keys()]);
for (const acctId of touched) {
+1 -1
View File
@@ -48,7 +48,7 @@ interface FileState {
selectedResources: Set<string>;
uploadProgress: UploadProgress | null;
client: IJMAPClient | null;
/** Which connected account's files are being browsed. Pro shell only null in single-account contexts. */
/** Which connected account's files are being browsed. Pro shell only - null in single-account contexts. */
currentAccountId: string | null;
clipboard: ClipboardState | null;
uploadAbortController: AbortController | null;
+4 -4
View File
@@ -155,7 +155,7 @@ export const usePluginStore = create<PluginStoreState>()(
}));
return;
} else {
// 'pending' or 'not-requested' submit a request and refuse to enable.
// 'pending' or 'not-requested' - submit a request and refuse to enable.
await submitApprovalRequest(plugin).catch(() => { /* best effort */ });
set(state => ({
plugins: state.plugins.map(p =>
@@ -165,12 +165,12 @@ export const usePluginStore = create<PluginStoreState>()(
return;
}
} else if (requireApproval && !policyApproved) {
// No bundleHash means we can't pin the approval refuse.
// No bundleHash means we can't pin the approval - refuse.
return;
}
// Per-user consent gate: prompt for any permission the user has not
// explicitly approved yet. Managed plugins (admin-pushed) skip this
// explicitly approved yet. Managed plugins (admin-pushed) skip this -
// the admin has already approved them at install time.
const implicit = new Set<string>(IMPLICIT_PERMISSIONS);
const granted = new Set<string>(plugin.grantedPermissions ?? []);
@@ -549,7 +549,7 @@ async function downloadPluginBundle(pluginId: string, bundleHash?: string): Prom
// Ed25519 signature verification. Present on every server-managed bundle
// since the signing module is server-side; refuse to persist a bundle
// that fails verification. If the header is missing (older server / dev
// build with signing disabled) we log and allow the SHA-256 hash check
// build with signing disabled) we log and allow - the SHA-256 hash check
// at load time still catches transport corruption.
const sig = res.headers.get('X-Bundle-Signature');
if (sig) {
+4 -4
View File
@@ -8,7 +8,7 @@ export type ProTabKind =
export type ProPaneId = 'main' | 'split';
/**
* Pro split layout. Only side-by-side is supported the pane that "splits
* Pro split layout. Only side-by-side is supported - the pane that "splits
* off" always lives next to the main pane on the horizontal axis. Kept as
* a type alias to leave room for future layouts without churning callers.
*/
@@ -17,7 +17,7 @@ export type ProSplitOrientation = 'vertical';
export type ProComposerMode = 'compose' | 'reply' | 'replyAll' | 'forward';
/**
* Mirror of `EmailComposer.replyTo` kept as a structural type here so the
* Mirror of `EmailComposer.replyTo` - kept as a structural type here so the
* tab store doesn't take a runtime dependency on the composer module.
*/
export interface ProReplyContext {
@@ -92,7 +92,7 @@ interface ProTabState {
/**
* Move a tab next to another tab. `edge` controls whether it lands before
* or after the target used by the tab bar's drop indicator. Reordering
* or after the target - used by the tab bar's drop indicator. Reordering
* works both within a pane and across panes (cross-pane drops move the
* tab to the target pane).
*/
@@ -476,7 +476,7 @@ export const useProTabStore = create<ProTabState>()(
{
name: 'pro-tabs',
version: 3,
// Don't persist transient compose drafts in tab metadata the composer's
// Don't persist transient compose drafts in tab metadata - the composer's
// own draft-store already handles that. Persisted email tabs are fine to
// restore (the tab body refetches the email by id).
partialize: (state) => ({
+3 -3
View File
@@ -45,7 +45,7 @@ export type ProtocolOpenMode = 'active-session' | 'new-tab';
/**
* Settings that must never round-trip through the cross-device sync API.
* Decided per device and kept only in the local zustand-persist storage
* Decided per device and kept only in the local zustand-persist storage -
* a value already stored on the server (from a prior build) is ignored on
* import.
*/
@@ -520,7 +520,7 @@ export const useSettingsStore = create<SettingsState>()(
toolbarPosition: state.toolbarPosition,
hideAccountSwitcher: state.hideAccountSwitcher,
showRailAccountList: state.showRailAccountList,
// proInterface is intentionally omitted it's a per-device choice
// proInterface is intentionally omitted - it's a per-device choice
// (see DEVICE_LOCAL_SETTING_KEYS) and must not be synced.
enableUnifiedMailbox: state.enableUnifiedMailbox,
senderFavicons: state.senderFavicons,
@@ -853,7 +853,7 @@ if (typeof window !== 'undefined') {
syncWarn('Settings sync endpoint returned 404, disabling sync');
syncEnabled = false;
} else if (res.status === 403) {
// Identity mismatch current session cookies don't match the
// Identity mismatch - current session cookies don't match the
// username/serverUrl we're syncing for (common in dev mock mode where
// no stalwart-context cookie is written, or when rememberMe is off).
// Retrying won't help for this session; disable to stop the noise.
+1 -1
View File
@@ -19,7 +19,7 @@ import {
// Legacy storage key used by an earlier build that persisted unlock passphrases
// in sessionStorage. Wipe on module load so any in-flight tab upgrading to this
// version doesn't leave plaintext key material sitting around. New code never
// writes here unlocked CryptoKey handles live only in the in-memory Map below.
// writes here - unlocked CryptoKey handles live only in the in-memory Map below.
const LEGACY_REMEMBERED_UNLOCKS_KEY = 'smime-unlocked-session';
if (typeof window !== 'undefined') {
try { window.sessionStorage.removeItem(LEGACY_REMEMBERED_UNLOCKS_KEY); } catch { /* ignore */ }