Skip to content

Commit 695a7fc

Browse files
committed
fix: gate resumeWorkflow with waitForKeys, prevent crypto migration loop
- Add waitForKeys() to resumeWorkflow() (4th public entry point missed) - Split writeEncryptedKeys (UI setter, plaintext fallback) from tryMigrateToEncrypted (migration only, no-op on crypto failure) - Prevents repeated failed migration attempts on every page load when SubtleCrypto/IndexedDB is unavailable
1 parent 35a9853 commit 695a7fc

2 files changed

Lines changed: 18 additions & 3 deletions

File tree

src/core/run-controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ class RunController {
367367

368368
async resumeWorkflow(workflowPath: string): Promise<void> {
369369
if (this.state.isRunning) return;
370+
await this.waitForKeys();
370371

371372
const content = vfsStore.getState().read(workflowPath);
372373
if (!content) return;

src/stores/use-stores.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,30 @@ async function loadCryptoKey(): Promise<CryptoKey> {
126126
if (!_cryptoKey) _cryptoKey = await getOrCreateKey();
127127
return _cryptoKey;
128128
}
129+
/** Write keys to localStorage, encrypted if possible, plaintext as fallback. */
129130
async function writeEncryptedKeys(keys: Record<string, string>): Promise<void> {
130131
try {
131132
const key = await loadCryptoKey();
132133
const encrypted = await encryptValue(key, JSON.stringify(keys));
133134
localStorage.setItem('mas-provider-api-keys', encrypted);
134135
} catch {
135-
// SubtleCrypto/IndexedDB unavailable — write plaintext as fallback
136+
// SubtleCrypto/IndexedDB unavailable — write plaintext so key changes aren't lost
136137
try { localStorage.setItem('mas-provider-api-keys', JSON.stringify(keys)); } catch { /* */ }
137138
}
138139
}
139140

141+
/** Attempt to encrypt existing plaintext keys. Returns true if migration succeeded. */
142+
async function tryMigrateToEncrypted(keys: Record<string, string>): Promise<boolean> {
143+
try {
144+
const key = await loadCryptoKey();
145+
const encrypted = await encryptValue(key, JSON.stringify(keys));
146+
localStorage.setItem('mas-provider-api-keys', encrypted);
147+
return true;
148+
} catch {
149+
return false;
150+
}
151+
}
152+
140153
export const uiStore = createStore<UIState>((set) => ({
141154
selectedAgentId: null,
142155
selectedFilePath: null,
@@ -238,8 +251,9 @@ export const uiStore = createStore<UIState>((set) => ({
238251
state.setProviderApiKeys(keys);
239252
}
240253
} else if (_rawProviderApiKeys) {
241-
// Stored value is plaintext JSON — migrate to encrypted
242-
await writeEncryptedKeys(persistedProviderApiKeys);
254+
// Stored value is plaintext JSON — try to migrate to encrypted.
255+
// If crypto is unavailable, leave plaintext as-is (no-op, no loop on next load).
256+
await tryMigrateToEncrypted(persistedProviderApiKeys);
243257
}
244258
} catch (err) {
245259
uiStore.getState().setKeysError(

0 commit comments

Comments
 (0)