fix(settings): remember browser credentials by default

- Default fresh browser sessions to encrypted credential persistence with an explicit session-only opt-out

- Keep API-key links left-aligned, legible, and limited to their text click target

- Update credential guidance and browser coverage
This commit is contained in:
Danila Poyarkov 2026-07-27 19:05:09 +03:00
parent 858e6451de
commit a7a44e90b2
6 changed files with 21 additions and 14 deletions

View file

@ -54,7 +54,7 @@ Property-panel anatomy in `packages/vue/src/primitives/PropertySection/`, `Segme
Credential persistence lives under `src/app/settings/credentials/`. Settings components receive `CredentialManager` and may inspect status, replace, or clear credentials; runtime adapters receive `CredentialResolver`. Components must not read saved secrets or keep them in long-lived reactive refs. Non-secret provider preferences remain in normal settings storage.
Tauri stores secrets in the native system credential store through `desktop/src/credentials.rs`; browser sessions default to memory and may explicitly opt into WebCrypto-encrypted IndexedDB storage. Native failures must never silently fall back to browser or plaintext storage. New integration credentials use stable `CredentialRef` values and join the unified Settings surface rather than adding feature-local key forms.
Tauri stores secrets in the native system credential store through `desktop/src/credentials.rs`; browsers default to WebCrypto-encrypted IndexedDB storage and may explicitly opt out to session-only memory. Native failures must never silently fall back to browser or plaintext storage. New integration credentials use stable `CredentialRef` values and join the unified Settings surface rather than adding feature-local key forms.
Storage-provider schemas and runtime adapters live under `src/app/integrations/storage/`; non-secret preferences and credential references stay separate, and adapters resolve secrets at operation time. Local-first document caching and outbox synchronization live under `src/app/storage/`. A remote storage binding augments document source state and must not replace local file identity.

View file

@ -19,7 +19,7 @@
- Drag with the Text tool to create a fixed-size text box, or click to create auto-width text.
- Target a specific open document and page from live CLI and MCP automation, including sessions with multiple documents.
- Test OpenAI-compatible provider connections from AI settings with clearer setup errors.
- Manage AI, agent, and media credentials from unified Settings, using the system credential store on desktop and optional encrypted storage in the browser.
- Manage AI, agent, and media credentials from unified Settings, using the system credential store on desktop and encrypted browser storage by default with a session-only option.
- Assign separate Design, Review, Fast, and Vision models, providers, endpoints, and credentials from AI settings.
- Connect an S3-compatible storage workspace with local-first saves, background synchronization, and centrally managed credentials.
- Convert image layers into editable vector layers with Recraft or fal.ai from the canvas context menu. (#322)

View file

@ -20,12 +20,12 @@ export function hasLegacyCredentialStorage(): boolean {
}
export function browserRemembersCredentials(): boolean {
return browserCredentialStorage()?.getItem(BROWSER_PERSISTENCE_KEY) === 'remembered'
const storage = browserCredentialStorage()
return storage ? storage.getItem(BROWSER_PERSISTENCE_KEY) !== 'session' : false
}
export function setBrowserRemembersCredentials(remembered: boolean): void {
const storage = browserCredentialStorage()
if (!storage) return
if (remembered) storage.setItem(BROWSER_PERSISTENCE_KEY, 'remembered')
else storage.removeItem(BROWSER_PERSISTENCE_KEY)
storage.setItem(BROWSER_PERSISTENCE_KEY, remembered ? 'remembered' : 'session')
}

View file

@ -6,7 +6,12 @@ const { href } = defineProps<{ href: string }>()
</script>
<template>
<AppTextButton size="xs" underline @click="openExternalLink(href)">
<AppTextButton
size="sm"
underline
:ui="{ base: 'self-start text-left text-[11px]' }"
@click="openExternalLink(href)"
>
<slot />
</AppTextButton>
</template>

View file

@ -150,9 +150,11 @@ test('saving API key in unified settings shows chat interface', async () => {
await expect(page.getByTestId('app-settings-dialog')).toBeVisible()
await expect(page.getByTestId('settings-remember-credentials')).toHaveAttribute(
'data-state',
'unchecked'
'checked'
)
await expect(page.getByTestId('settings-credential-backend')).toContainText(
'encrypted browser storage'
)
await expect(page.getByTestId('settings-credential-backend')).toContainText('this session only')
await page.locator('[data-model-id]').first().click()
await page.getByTestId('settings-model-provider').click()
await page.getByRole('option', { name: 'OpenRouter' }).click()
@ -270,10 +272,12 @@ test('transport errors show an actionable toast', async () => {
})
test('"Get API key" link opens external URL via window.open', async () => {
await page.evaluate("localStorage.removeItem('open-pencil:ai-key:openrouter')")
await page.reload()
await canvas.waitForInit()
await chatTab().click()
await page.getByTestId('provider-settings-trigger').click()
await page.locator('[data-model-id]').first().click()
await page.getByTestId('provider-settings-clear-key').click()
await page.getByRole('button', { name: 'Back' }).click()
await page.getByTestId('app-settings-done').click()
await expect(page.getByTestId('provider-setup-open-settings')).toBeVisible()
await page.getByTestId('provider-setup-open-settings').click()
await page.locator('[data-model-id]').first().click()
await page.getByTestId('settings-model-provider').click()

View file

@ -80,8 +80,6 @@ test('remembered browser credentials survive reload and clear centrally', async
await page.getByTestId('provider-setup-open-settings').click()
const remember = page.getByTestId('settings-remember-credentials')
await expect(remember).toHaveAttribute('data-state', 'unchecked')
await remember.click()
await expect(remember).toHaveAttribute('data-state', 'checked')
await expect(page.getByTestId('settings-credential-backend')).toContainText(
'encrypted browser storage'