feat(portable): add secure Windows portable release (#15)

Implement marker-based portable storage, portable WebView and log paths, secure queue and migration sanitization, and Windows portable ZIP validation while preserving the NSIS installer path.

Refs #15
This commit is contained in:
NimBold
2026-07-12 23:08:48 +03:30
parent 56b4c9f511
commit a0f44b79ad
20 changed files with 905 additions and 82 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PlatformInfo = { os: string, arch: string, targetTriple: string, };
export type PlatformInfo = { os: string, arch: string, targetTriple: string, portable: boolean, };
+20 -7
View File
@@ -17,14 +17,20 @@ export const KeychainPermissionModal: React.FC = () => {
const isMac = platform.os === 'macos';
const storeName =
platform.os === 'windows'
platform.portable
? 'the portable Firelink data folder'
: platform.os === 'windows'
? 'Windows Credential Manager'
: platform.os === 'linux'
? 'your Linux credential store'
: platform.os === 'macos'
? 'macOS Keychain'
: "this system's credential store";
const grantLabel = isMac ? 'Grant Access' : 'Enable Secure Storage';
const grantLabel = platform.portable
? 'Enable Portable Pairing'
: isMac
? 'Grant Access'
: 'Enable Secure Storage';
const handleGrant = async () => {
setIsGranting(true);
@@ -75,14 +81,18 @@ export const KeychainPermissionModal: React.FC = () => {
</p>
<p>
{isMac
{platform.portable
? 'The pairing token is portable with this folder. Treat the folder as sensitive and do not share it.'
: isMac
? 'macOS may show a Keychain prompt after you grant access.'
: 'This usually completes silently. If the credential service is unavailable, Firelink will show the error here and the extension will stay paired for this session only.'}
</p>
<p>
<strong>Note:</strong> Firelink only writes its own dedicated credential entry. It cannot access other
saved passwords or credential items on your system.
<strong>Note:</strong>{' '}
{platform.portable
? 'Portable mode stores only the pairing token in this folder. It does not copy site passwords or browser credentials.'
: 'Firelink only writes its own dedicated credential entry. It cannot access other saved passwords or credential items on your system.'}
</p>
{error && (
@@ -93,8 +103,11 @@ export const KeychainPermissionModal: React.FC = () => {
)}
<div className="bg-bg-modal-accent p-3 rounded-lg border border-border-modal text-xs">
<strong>Hint:</strong> If you select Later, the extension will only work for this session.
You can enable secure storage anytime from <strong>Settings &gt; Integrations</strong>.
<strong>Hint:</strong>{' '}
{platform.portable
? 'The portable pairing token is already stored with this folder; you can enable it here or select Later.'
: 'If you select Later, the extension will only work for this session.'}
You can enable storage anytime from <strong>Settings &gt; Integrations</strong>.
</div>
</div>
+13 -6
View File
@@ -1214,9 +1214,13 @@ className="app-button px-3 py-1.5 text-[12px] flex items-center gap-1.5 disabled
<Check size={16} strokeWidth={2.5} />
</div>
<div className="flex-1">
<h4 className="text-sm font-semibold text-green-500 m-0">Credential Storage Available</h4>
<h4 className="text-sm font-semibold text-green-500 m-0">
{platform.portable ? 'Portable Pairing Enabled' : 'Credential Storage Available'}
</h4>
<p className="text-xs text-text-secondary m-0 mt-0.5">
Your pairing token is securely saved in this system's credential store and will persist across restarts.
{platform.portable
? 'Your pairing token is stored with this portable Firelink folder and will persist when the folder is moved. Treat the folder as sensitive.'
: "Your pairing token is securely saved in this system's credential store and will persist across restarts."}
</p>
</div>
</div>
@@ -1224,16 +1228,19 @@ className="app-button px-3 py-1.5 text-[12px] flex items-center gap-1.5 disabled
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 flex items-start gap-3">
<ShieldAlert className="w-5 h-5 text-orange-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="text-sm font-semibold text-text-primary mb-1">Credential Storage Needed</h4>
<h4 className="text-sm font-semibold text-text-primary mb-1">
{platform.portable ? 'Portable Pairing Available' : 'Credential Storage Needed'}
</h4>
<p className="text-xs text-text-secondary mb-3">
Firelink needs access to this system's credential store to securely save your pairing token across app restarts.
Currently, your extension will only stay connected for this session.
{platform.portable
? 'Your pairing token is stored with this portable Firelink folder and will persist across restarts. Enable it here to review the portable-storage warning.'
: "Firelink needs access to this system's credential store to securely save your pairing token across app restarts. Currently, your extension will only stay connected for this session."}
</p>
<button
onClick={() => settings.setShowKeychainModal(true)}
className="px-4 py-1.5 rounded-md text-xs font-medium transition-colors bg-accent text-white hover:bg-accent/90 shadow-sm"
>
Grant Credential Access
{platform.portable ? 'Review Portable Pairing' : 'Grant Credential Access'}
</button>
</div>
</div>
+3 -1
View File
@@ -53,7 +53,9 @@ const tauriStorage: StateStorage = {
* Keychain identifier for the browser-extension pairing token. The token is an
* HMAC shared secret and is therefore persisted via the OS keychain rather
* than the user-data database. Legacy plaintext values are migrated into the
* Keychain before being removed from persisted settings.
* Keychain before being removed from persisted settings. Portable mode is the
* explicit exception: its pairing token is persisted with the portable folder
* so extension pairing follows that folder.
*/
const PAIRING_TOKEN_KEYCHAIN_ID = 'extension-pairing-token';
+4 -5
View File
@@ -121,11 +121,10 @@ const DOWNLOAD_SECRET_FIELDS = ['password', 'cookies', 'headers'] as const;
* progress fields (`fraction`, `speed`, `eta`) are also dropped as in the
* existing persistence path.
*
* Note: `url` is intentionally retained even though it may contain signed
* query parameters — redacting it would break resume/retry since the URL is
* the download source. Ad-hoc credentials entered in the Add Downloads modal
* are therefore session-scoped; site-login passwords (Keychain-backed) are
* unaffected by this redaction.
* Note: standard persistence intentionally retains `url` because it is the
* download source. The backend applies a stricter portable-mode policy: URL
* userinfo, query, and fragment components are removed before portable data
* is written, and affected active records are not auto-resumed.
*/
export const redactDownloadForPersistence = (item: DownloadItem): DownloadItem => {
const copy: DownloadItem = { ...item };
+2 -1
View File
@@ -5,7 +5,8 @@ import { invokeCommand as invoke } from '../ipc';
const fallback: PlatformInfo = {
os: 'unknown',
arch: 'unknown',
targetTriple: 'unknown'
targetTriple: 'unknown',
portable: false
};
let cached: PlatformInfo | null = null;