feat: expose PWA, app identity, and extension directory keys in JSON config #312
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 */ }
|
||||
|
||||
Reference in New Issue
Block a user