chore: update version to 1.6.2
This commit is contained in:
@@ -1,5 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
## 1.6.2 (2026-05-06)
|
||||
|
||||
### Features
|
||||
|
||||
- **Plugins**: Hot-reload and dev-folder loading for live plugin development
|
||||
- **Plugins**: On-demand `src/` bundling via esbuild
|
||||
- **Plugins**: New `http:fetch` permission and `httpOrigins` manifest field
|
||||
- **Plugins**: `onBeforeEmailSend` hook with `fromEmail` exposed on `OutgoingEmail`
|
||||
- **Plugins**: Project `EmailReadView` for the email-banner slot and expose auth results
|
||||
- **Plugins**: Ingest icon, banner, and screenshots from the source repo
|
||||
- **Plugins**: Restrict plugin and theme install/uninstall to the admin dashboard
|
||||
- **Mail**: Multi-server JMAP support
|
||||
- **Settings**: Fulltext search across the settings sidebar
|
||||
- **Settings**: Sub-result rows with highlight in settings search
|
||||
- **Settings**: Surface plugin settings as search sub-results
|
||||
- **Settings**: Remove experimental tags from themes, plugins, and sender favicons
|
||||
- **Viewer**: Redesigned external-mail banner above attachments
|
||||
- **Calendar**: Calendar invitation banner expands on row click
|
||||
- **Calendar**: Calendar invitation banner is now collapsible
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Admin**: Collapse admin panel into a single tabbed page
|
||||
- **Plugins**: Inline plugin configure panel to avoid dev-mode hang
|
||||
- **Plugins**: Resolve `PLUGIN_DEV_DIR` plugins in admin config route
|
||||
- **Plugins**: Add missing body type assertion in `createPluginAPI` fetch options
|
||||
- **Plugins**: Propagate `settingsSchema`
|
||||
- **Settings**: Highlight plugin and theme cards in search results
|
||||
- **Settings**: Open plugin card on first click of a setting sub-result
|
||||
- **Settings**: Drop ghost sub-results from account and language search
|
||||
- **Settings**: Improve search highlight styling
|
||||
- **Viewer**: Show notification banners above attachments
|
||||
- **Viewer**: Rework S/MIME banner to match calendar invitation
|
||||
- **Viewer**: Close PDF preview on Escape before email viewer
|
||||
- **Viewer**: Render PDF previews via `<object>` with `blob:` in object-src CSP (#253)
|
||||
- **Calendar**: Align invitation icon with sender avatar column
|
||||
- **Calendar**: Fix invitation picker clipping (#250)
|
||||
- **Auth**: Read `activeAccountId` from authStore in account selectors
|
||||
- **UI**: Adjust toast item border radius and progress bar styles
|
||||
- **UI**: Remove fly-in animation from context menu submenus
|
||||
- **i18n**: Add missing Czech flag icon
|
||||
|
||||
### i18n
|
||||
|
||||
- Add missing translation keys across 15 locales
|
||||
|
||||
## 1.6.1 (2026-05-04)
|
||||
|
||||
### Features
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ARG NEXT_PUBLIC_BASE_PATH=
|
||||
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
|
||||
# Commit SHA shown in the About screen. .dockerignore excludes .git, so
|
||||
# `git rev-parse` inside the build can't find it — CI must pass it in.
|
||||
# `git rev-parse` inside the build can't find it - CI must pass it in.
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
RUN npx next build --webpack
|
||||
|
||||
@@ -543,7 +543,7 @@ export default function LoginPage() {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Server-list entries always win — `allowCustomJmapEndpoint` is only honored
|
||||
// Server-list entries always win - `allowCustomJmapEndpoint` is only honored
|
||||
// when the admin hasn't configured a server list.
|
||||
const effectiveServerUrl = selectedServer?.url
|
||||
|| (allowCustomJmapEndpoint ? jmapEndpoint : serverUrl);
|
||||
|
||||
@@ -740,7 +740,7 @@ export default function Home() {
|
||||
|
||||
// System-notification click handler. The push SW navigates the user back
|
||||
// here with `?email=<id>` (specific email it built the toast from) or
|
||||
// `?openLatestUnread=1` (generic "New mail" toast — happens when the
|
||||
// `?openLatestUnread=1` (generic "New mail" toast - happens when the
|
||||
// preview API failed). We resolve those params once after the inbox has
|
||||
// finished loading and open the right message, then strip the params so a
|
||||
// refresh doesn't re-open it.
|
||||
@@ -1610,7 +1610,7 @@ export default function Home() {
|
||||
|
||||
const originalEmailId = selectedEmail.id;
|
||||
|
||||
// RFC 5322 §3.6.4 threading — keep the conversation stitched together (#234).
|
||||
// RFC 5322 §3.6.4 threading - keep the conversation stitched together (#234).
|
||||
const threading = computeReplyThreadingHeaders({
|
||||
messageId: selectedEmail.messageId,
|
||||
references: selectedEmail.references,
|
||||
@@ -1674,7 +1674,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
// Show the list stub immediately so subject/sender render without
|
||||
// waiting for the body fetch — avoids the loading flicker.
|
||||
// waiting for the body fetch - avoids the loading flicker.
|
||||
const listEmail = emails.find(e => e.id === email.id);
|
||||
if (listEmail) {
|
||||
selectEmail(listEmail);
|
||||
|
||||
@@ -491,7 +491,7 @@ export default function SettingsPage() {
|
||||
// For plugin-setting sub-results, ask the plugins tab to expand the
|
||||
// matching card so the field becomes part of the DOM. Dispatched here
|
||||
// (not in the click handler) because PluginsSettings only mounts after
|
||||
// the tab switches, and its listener registers in its own useEffect —
|
||||
// the tab switches, and its listener registers in its own useEffect -
|
||||
// child effects run before parent effects, so by the time we get here
|
||||
// the listener is guaranteed to be in place.
|
||||
if (pendingHighlight.pluginId) {
|
||||
@@ -534,7 +534,7 @@ export default function SettingsPage() {
|
||||
// First attempt next frame so the freshly-mounted tab content is in DOM.
|
||||
const raf = window.requestAnimationFrame(tryHighlight);
|
||||
|
||||
// Do NOT reset pendingHighlight here — that would retrigger this effect
|
||||
// Do NOT reset pendingHighlight here - that would retrigger this effect
|
||||
// and the cleanup below would strip the class right after we added it.
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// Inline panel handles plugin config now — see _tabs/plugin-config-panel.tsx.
|
||||
// Inline panel handles plugin config now - see _tabs/plugin-config-panel.tsx.
|
||||
// Old deep links land on the plugins tab; the user clicks the gear again.
|
||||
export default function Page() {
|
||||
redirect('/admin?tab=plugins');
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function GET() {
|
||||
|
||||
/**
|
||||
* POST /api/admin/version
|
||||
* { action: 'check-now' } — force a fresh upstream fetch.
|
||||
* { action: 'check-now' } - force a fresh upstream fetch.
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function POST(request: NextRequest) {
|
||||
// caller cannot point this route at internal hosts. We accept the global
|
||||
// `jmapServerUrl` and any entry from `jmapServers`. When neither matches,
|
||||
// we fall back to the request URL only if `allowCustomJmapEndpoint` is on
|
||||
// — and even then the URL must resolve to a public address.
|
||||
// - and even then the URL must resolve to a public address.
|
||||
await configManager.ensureLoaded();
|
||||
const configuredServerUrl =
|
||||
configManager.get<string>('jmapServerUrl', '') ||
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function POST(request: NextRequest) {
|
||||
// Pin the upstream URL to a configured JMAP server. The list of allowed
|
||||
// servers is `jmapServerUrl` plus any entry from `jmapServers`. Only when
|
||||
// no server is configured (and the deployment explicitly allows custom
|
||||
// JMAP endpoints) do we fall back to the user-supplied URL — and even then
|
||||
// JMAP endpoints) do we fall back to the user-supplied URL - and even then
|
||||
// it must resolve to a public address.
|
||||
await configManager.ensureLoaded();
|
||||
const configuredServerUrl =
|
||||
|
||||
@@ -163,7 +163,7 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// `fetch failed` from undici is too generic to debug — the real reason
|
||||
// `fetch failed` from undici is too generic to debug - the real reason
|
||||
// (ENOTFOUND, ECONNREFUSED, TLS error, …) is on `error.cause`.
|
||||
const err = error as Error & { cause?: { code?: string; message?: string } };
|
||||
logger.error('push preview failed', {
|
||||
|
||||
@@ -963,7 +963,7 @@ export function EmailComposer({
|
||||
return '';
|
||||
};
|
||||
|
||||
// RFC 5322 §3.6.4 threading — only continues the chain on a reply, not a forward.
|
||||
// RFC 5322 §3.6.4 threading - only continues the chain on a reply, not a forward.
|
||||
const threadingHeaders = (mode === 'reply' || mode === 'replyAll')
|
||||
? computeReplyThreadingHeaders(replyTo)
|
||||
: null;
|
||||
|
||||
@@ -2697,7 +2697,7 @@ export function EmailViewer({
|
||||
const colorScheme = isDark && emailHasNativeDarkMode ? 'light dark' : 'light';
|
||||
|
||||
// Bare HTML emails (no <style>) tend to be plain prose without their own
|
||||
// layout — give them the same padding as plain-text mails (.email-content-text).
|
||||
// layout - give them the same padding as plain-text mails (.email-content-text).
|
||||
const bodyPadding = effectiveEmailContent.hasStyleTag ? '0' : '1rem 1.25rem';
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
@@ -2759,7 +2759,7 @@ export function EmailViewer({
|
||||
}, []);
|
||||
|
||||
// Whenever permission is granted (allow toggled, or sender becomes trusted),
|
||||
// restore blocked content in the existing iframe — no srcDoc rebuild.
|
||||
// restore blocked content in the existing iframe - no srcDoc rebuild.
|
||||
const senderEmailLower = email?.from?.[0]?.email?.toLowerCase();
|
||||
const senderIsTrustedNow = senderEmailLower
|
||||
? isSenderTrusted(senderEmailLower) || (trustedSendersAddressBook && isTrustedAddressBookSender(senderEmailLower))
|
||||
@@ -2771,7 +2771,7 @@ export function EmailViewer({
|
||||
}, [allowExternalContent, senderIsTrustedNow, hasBlockedContent, restoreBlockedContent]);
|
||||
|
||||
// Tracks the last rendered body height so the loading skeleton can hold
|
||||
// the same size — avoids the body shrink/expand flash when switching emails.
|
||||
// the same size - avoids the body shrink/expand flash when switching emails.
|
||||
const lastBodyHeightRef = useRef<number>(300);
|
||||
|
||||
// True while the new email's body is still being fetched. Catches the
|
||||
@@ -4022,7 +4022,7 @@ export function EmailViewer({
|
||||
};
|
||||
const fullDate = (iso?: string) => iso
|
||||
? formatDateTime(iso, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' })
|
||||
: '—';
|
||||
: '-';
|
||||
const auth = email.authenticationResults;
|
||||
const totalAttachmentSize = effectiveAttachments.reduce((s, a) => s + (a.size || 0), 0);
|
||||
const topMimeType = email.bodyStructure?.type;
|
||||
|
||||
@@ -185,7 +185,7 @@ export function NavigationRail({
|
||||
// Account list for rail
|
||||
const accounts = useAccountStore((s) => s.accounts);
|
||||
// Read activeAccountId from authStore so the rail's account row matches the actually-loaded
|
||||
// session — accountStore has its own persisted copy that can drift out of sync.
|
||||
// session - accountStore has its own persisted copy that can drift out of sync.
|
||||
const activeAccountId = useAuthStore((s) => s.activeAccountId);
|
||||
const switchAccount = useAuthStore((s) => s.switchAccount);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
|
||||
@@ -64,7 +64,7 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
||||
// If the resume step is beyond the current steps, start from 0
|
||||
if (resumeStep >= steps.length) resumeStep = 0;
|
||||
|
||||
// Most steps live on the mailbox — navigate there (or to the step's specific page)
|
||||
// Most steps live on the mailbox - navigate there (or to the step's specific page)
|
||||
// so we don't start the tour on a page where the targets don't exist.
|
||||
const targetPage = steps[resumeStep]?.page ?? "/";
|
||||
if (pathname !== targetPage) {
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('JMAPClient.sendEmail threading headers', () => {
|
||||
const create = setCall[1].create as Record<string, Record<string, unknown>>;
|
||||
const draft = Object.values(create)[0];
|
||||
|
||||
// Bare msg-ids per RFC 8621 — angle brackets stripped.
|
||||
// Bare msg-ids per RFC 8621 - angle brackets stripped.
|
||||
expect(draft.inReplyTo).toEqual(['parent@example.com']);
|
||||
expect(draft.references).toEqual(['root@example.com', 'parent@example.com']);
|
||||
});
|
||||
|
||||
@@ -162,7 +162,7 @@ export function resolveTrustedJmapUrl(
|
||||
}
|
||||
const matched = servers.find((s) => normalizeUrl(s.url) === target);
|
||||
if (matched) return matched.url;
|
||||
// No match — caller decides whether to honor the request anyway (e.g. when
|
||||
// No match - caller decides whether to honor the request anyway (e.g. when
|
||||
// allowCustomJmapEndpoint is enabled).
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ function resolveBundlePath(pluginDir: string, entrypoint: string): ResolvedBundl
|
||||
/**
|
||||
* Load and bundle a dev plugin's code. For `src/` sources this runs esbuild
|
||||
* on every call so saves are reflected immediately. Errors are surfaced as
|
||||
* a JS module that throws at activation time — that way the dev sees the
|
||||
* a JS module that throws at activation time - that way the dev sees the
|
||||
* failure in the browser console instead of a silent 404.
|
||||
*/
|
||||
export async function readDevBundle(entry: DevPluginEntry): Promise<string> {
|
||||
@@ -98,7 +98,7 @@ export async function readDevBundle(entry: DevPluginEntry): Promise<string> {
|
||||
sourcemap: 'inline',
|
||||
target: ['es2020'],
|
||||
// React/ReactDOM are exposed on globalThis.__PLUGIN_EXTERNALS__ by the
|
||||
// host, so we mark them external — the bundle won't try to ship them.
|
||||
// host, so we mark them external - the bundle won't try to ship them.
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||||
});
|
||||
const out = result.outputFiles?.[0]?.text;
|
||||
@@ -143,7 +143,7 @@ async function loadDevPlugin(pluginDir: string): Promise<DevPluginEntry | null>
|
||||
}
|
||||
|
||||
// Hash from the on-disk source so any edit propagates. For src/ sources
|
||||
// we hash the source — close enough for dev-time change detection (we
|
||||
// we hash the source - close enough for dev-time change detection (we
|
||||
// don't need to re-hash transitive imports).
|
||||
let bundleHash: string;
|
||||
try {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* In-Reply-To = parent.Message-ID
|
||||
* References = parent.References (if any) + parent.Message-ID
|
||||
*
|
||||
* Bare msg-ids only — angle brackets are stripped because JMAP RFC 8621
|
||||
* Bare msg-ids only - angle brackets are stripped because JMAP RFC 8621
|
||||
* §4.1.2.3 stores Message-IDs without them.
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -304,7 +304,7 @@ function stripMessageIdBrackets(id: string): string {
|
||||
// form: `Display Name <addr@example.com>`. Re-emitting that as the JMAP
|
||||
// from.name field produces a doubled From header (`"Name <addr>" <addr>`)
|
||||
// whose display-name is invalid per RFC 5322 §3.4 and gets rejected by the
|
||||
// submission validator — the email then sits forever in Drafts.
|
||||
// submission validator - the email then sits forever in Drafts.
|
||||
function sanitizeIdentityDisplayName(name: string | undefined | null): string {
|
||||
if (!name) return '';
|
||||
return name.replace(/\s*<[^>]*>\s*$/, '').trim();
|
||||
|
||||
+3
-3
@@ -115,7 +115,7 @@ function createPluginLogger(pluginId: string) {
|
||||
* declared `httpOrigins` patterns. Patterns are either a literal origin
|
||||
* (`https://host[:port]`) or a wildcard subdomain form (`https://*.host`).
|
||||
*
|
||||
* Wildcards match exactly one subdomain layer above `host` — e.g.
|
||||
* Wildcards match exactly one subdomain layer above `host` - e.g.
|
||||
* `https://*.example.com` matches `https://a.example.com` but NOT
|
||||
* `https://example.com` and NOT `https://a.b.example.com`. This mirrors how
|
||||
* the CSP frame-src handles wildcards and avoids accidentally widening
|
||||
@@ -153,7 +153,7 @@ function originMatchesAllowlist(url: URL, allowlist: string[]): boolean {
|
||||
export interface PluginFetchInit {
|
||||
/** HTTP method. Defaults to GET. */
|
||||
method?: string;
|
||||
/** Request headers. Plain object only — no Headers / cookies forwarded. */
|
||||
/** Request headers. Plain object only - no Headers / cookies forwarded. */
|
||||
headers?: Record<string, string>;
|
||||
/** Body. Plain string, ArrayBuffer, Uint8Array, Blob, or FormData. */
|
||||
body?: string | ArrayBuffer | ArrayBufferView | Blob | FormData | null;
|
||||
@@ -210,7 +210,7 @@ export interface PluginAPI {
|
||||
* Cross-origin fetch against an origin declared in the manifest's
|
||||
* `httpOrigins` allowlist. Requires `http:fetch` permission.
|
||||
*
|
||||
* No webmail credentials are forwarded — the plugin must supply its own
|
||||
* No webmail credentials are forwarded - the plugin must supply its own
|
||||
* `Authorization` (or other auth) header. Each call is gated on origin
|
||||
* even when the URL came from plugin settings, so a user-pasted URL
|
||||
* outside the allowlist is rejected at the boundary.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Projection helpers — convert host-internal types into the read-only views
|
||||
// Projection helpers - convert host-internal types into the read-only views
|
||||
// that plugins consume. Keeping this in one place ensures every slot/hook
|
||||
// hands plugins the same shape declared in plugin-types.ts.
|
||||
|
||||
|
||||
+1
-1
@@ -678,7 +678,7 @@ export interface SelectionContext {
|
||||
export interface ConflictWarning {
|
||||
/** Stable unique key per warning, used as React key */
|
||||
key: string;
|
||||
/** Short message — e.g. "Conflicts with: Team Standup" */
|
||||
/** Short message - e.g. "Conflicts with: Team Standup" */
|
||||
message: string;
|
||||
severity?: 'info' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function isSupportedSubAddressDelimiter(value: string): value is SubAddre
|
||||
}
|
||||
|
||||
// RFC 5321 atext "special" characters, minus alphanumerics and "@". A custom
|
||||
// delimiter must be exactly one of these — they're safe to embed in a local
|
||||
// delimiter must be exactly one of these - they're safe to embed in a local
|
||||
// part and unambiguously separate the user from the tag.
|
||||
const VALID_DELIMITER_REGEX = /^[!#$%&'*+\-./=?^_`{|}~]$/;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function fetchStatus(
|
||||
if (!endpoint) return { ok: false, error: 'endpoint blank' };
|
||||
if (!currentVersion) return { ok: false, error: 'current version blank' };
|
||||
|
||||
// Build the URL safely — never inject the version as a raw path component.
|
||||
// Build the URL safely - never inject the version as a raw path component.
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(endpoint);
|
||||
|
||||
@@ -71,7 +71,7 @@ async function tick(): Promise<void> {
|
||||
await scheduleNext(delay);
|
||||
}
|
||||
|
||||
// Idempotent — safe to call from instrumentation hot-reload in dev.
|
||||
// Idempotent - safe to call from instrumentation hot-reload in dev.
|
||||
export async function startScheduler(): Promise<void> {
|
||||
if (disabledByEnv()) {
|
||||
logger.info('version-check: scheduler not started (disabled by env)');
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ const SUBSCRIPTION_REFRESH_THRESHOLD_DAYS = 7;
|
||||
|
||||
// Only `EmailDelivery` state-changes when new mail is actually delivered.
|
||||
// `Email` fires for any mutation (sending, drafting, moving, marking read,
|
||||
// deleting) and `Mailbox` fires for mailbox edits — both produced spurious
|
||||
// deleting) and `Mailbox` fires for mailbox edits - both produced spurious
|
||||
// system notifications, so we keep them out of the push subscription.
|
||||
// In-app sync uses a separate StateChange channel and is unaffected.
|
||||
const PUSH_TYPES = ['EmailDelivery'] as const;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bulwark-webmail",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bulwark-webmail",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "^3.13.24",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bulwark-webmail",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"description": "Bulwark Webmail - a modern webmail client built for Stalwart Mail Server",
|
||||
"author": "Bulwark Webmail <bulwark@rbm.systems>",
|
||||
"license": "AGPL-3.0-only",
|
||||
|
||||
@@ -408,7 +408,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ serverUrl, username, password: effectivePassword, slot: cookieSlot }),
|
||||
// Note: server_id isn't passed here — the route looks up the
|
||||
// Note: server_id isn't passed here - the route looks up the
|
||||
// server entry by serverUrl, so per-server OAuth still applies
|
||||
// for password+TOTP logins through the dropdown.
|
||||
});
|
||||
@@ -607,7 +607,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
// Determine slot for this account (use slot from sessionStorage if re-adding).
|
||||
// Note: `parseInt(getItem(...) || '0')` collapses "no value set" and
|
||||
// "value is 0" into the same case, so the fallback to getNextCookieSlot()
|
||||
// never fired for the common "+ Add Account" path — every OAuth account
|
||||
// never fired for the common "+ Add Account" path - every OAuth account
|
||||
// ended up on slot 0 and overwrote earlier accounts' refresh-token cookies.
|
||||
// Distinguishing rawSlot === null from a parsed 0 fixes that. The page
|
||||
// also writes oauth_cookie_slot before redirecting to the IdP.
|
||||
@@ -809,7 +809,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
isDefault: accountStore.accounts.length === 0,
|
||||
});
|
||||
// The refresh-token cookie was written to `slot` by /api/auth/sso/complete.
|
||||
// Force the stored cookieSlot to match — see loginWithOAuth above for the
|
||||
// Force the stored cookieSlot to match - see loginWithOAuth above for the
|
||||
// re-add and concurrent-tab cases this guards against.
|
||||
accountStore.updateAccount(accountId, { cookieSlot: slot });
|
||||
accountStore.setActiveAccount(accountId);
|
||||
@@ -1241,7 +1241,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
for (const account of accounts) {
|
||||
if (clients.has(account.id)) continue; // Already connected
|
||||
|
||||
// Basic auth without rememberMe leaves nothing to restore — the
|
||||
// Basic auth without rememberMe leaves nothing to restore - the
|
||||
// user logged in without persisting credentials. Evict silently
|
||||
// so the login screen is shown without flagging a fake error.
|
||||
if (account.authMode === 'basic' && !account.rememberMe) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const useUpdateStore = create<UpdateState>()((set, get) => ({
|
||||
lastFetchedAt: Date.now(),
|
||||
});
|
||||
} catch {
|
||||
// Silent — banner just won't appear, no need to disrupt the UI.
|
||||
// Silent - banner just won't appear, no need to disrupt the UI.
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
inFlight = null;
|
||||
|
||||
Reference in New Issue
Block a user