Skip to content

Conversation

@chhoumann
Copy link
Owner

@chhoumann chhoumann commented Jan 19, 2026

Summary

  • move AI provider API keys to Obsidian SecretStorage and resolve keys at runtime
  • migrate existing provider keys into SecretStorage and update docs/settings UI
  • update obsidian typings to 1.11.4 to pick up SecretStorage APIs

Testing

  • bun run test
  • bun run build-with-lint

Summary by CodeRabbit

  • New Features

    • API keys are now stored in SecretStorage; UIs let you select/create secrets and use secret-backed keys for provider connections and model syncs. Legacy keys migrate automatically.
  • Documentation

    • Docs updated to describe secret-based API key workflow, updated UI labels, and an embedded example video.
  • Migrations

    • Added migration to move existing provider keys into SecretStorage; migration flag added to settings.
  • Chores

    • Bumped Obsidian compatibility to v1.11.4.

✏️ Tip: You can customize this high-level summary in your review settings.


Open with Devin

@vercel
Copy link

vercel bot commented Jan 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
quickadd Ready Ready Preview Jan 22, 2026 3:39pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Switches AI provider API keys to Obsidian SecretStorage: adds apiKeyRef, secret utilities, UI SecretComponent integration, migration to move legacy keys to secrets, and threads resolved secrets into model discovery and runtime AI calls; docs, tests, and manifest/devDependency updated.

Changes

Cohort / File(s) Summary
Core provider shape & secrets
src/ai/Provider.ts, src/ai/providerSecrets.ts
Add apiKeyRef?: string to AIProvider; implement resolveProviderApiKey(app, provider) and storeProviderApiKeyInSecretStorage(...) with deterministic secret naming, fallbacks, and non-throwing SecretStorage handling.
Model discovery service
src/ai/modelDiscoveryService.ts
Change discoverProviderModels(provider) → `discoverProviderModels(provider, apiKeyOverride?: string
UI — provider modals & pickers
src/gui/AIAssistantProvidersModal.ts, src/gui/ProviderPickerModal.ts, src/gui/ModelDirectoryModal.ts
Replace plaintext API key inputs with Obsidian SecretComponent; add apiKeyRef wiring and legacy-key migration logic; resolve secrets before model sync/discovery and adjust modal resolution flow.
Runtime call sites
src/engine/MacroChoiceEngine.ts, src/quickAddApi.ts
Resolve provider API keys via resolveProviderApiKey(...) at runtime and pass resolved keys into AI assistant and prompt flows instead of using provider.apiKey.
Migrations & settings
src/migrations/migrate.ts, src/migrations/migrateProviderApiKeysToSecretStorage.ts, src/settings.ts
Add migration to move legacy provider API keys into SecretStorage, register it in migrations, and add migrateProviderApiKeysToSecretStorage flag to settings with default false.
Docs & dependency bump
docs/docs/AIAssistant.md, package.json, manifest.json, versions.json
Update documentation and UI labels to reflect secret-based API key flow; bump devDependency/minAppVersion to Obsidian 1.11.4 and update versions mapping.
Tests / stubs
tests/obsidian-stub.ts
Add SecretComponent test UI stub and an in-memory App.secretStorage API (getSecret, setSecret, listSecrets, delete) for tests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as "Provider Modal / Picker"
    participant AppSecret as "Obsidian SecretStorage"
    participant Secrets as "providerSecrets.ts"
    participant Discovery as "modelDiscoveryService"
    participant Provider as "AI Provider"

    User->>UI: select/create provider & choose secret
    UI->>AppSecret: setSecret(name, apiKey)
    AppSecret-->>UI: confirm stored
    UI->>Secrets: resolveProviderApiKey(app, provider)
    Secrets->>AppSecret: getSecret(provider.apiKeyRef)
    AppSecret-->>Secrets: return apiKey
    Secrets-->>UI: resolved apiKey
    UI->>Discovery: discoverProviderModels(provider, apiKeyOverride=resolvedKey)
    Discovery->>Provider: request models using apiKeyOverride
    Provider-->>Discovery: models list
    Discovery-->>UI: models
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Secrets snug beneath the loam,

Keys tucked safe, no need to roam,
Migration hops with careful art,
SecretComponent plays its part,
AI whispers from the hidden home. 🔑✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: SecretStorage for AI provider keys' directly and concisely describes the main change: moving AI provider API keys to Obsidian SecretStorage, which is the primary focus across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa61c0a532

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/ai/providerSecrets.ts`:
- Around line 73-74: The call to app.secretStorage.setSecret inside
storeProviderApiKeyInSecretStorage must be wrapped in a try/catch so the
function honors its Promise<string | null> contract; change the logic around the
existing await Promise.resolve(app.secretStorage.setSecret(candidate, trimmed))
to try to set the secret, catch any thrown error, log the failure using the same
pattern used for getSecret (include error details), and return null on
failure—otherwise return the candidate string on success.
🧹 Nitpick comments (2)
src/migrations/migrateProviderApiKeysToSecretStorage.ts (2)

23-68: Consider cloning providers before mutation.

The migration mutates provider objects directly (lines 36, 39-40, 59-60), but these are references from settingsStore.getState(). If the store returns frozen objects or uses immutable patterns, this could fail silently. Consider cloning the providers array upfront to ensure safe mutation.

♻️ Suggested approach
 		const currentSettings = settingsStore.getState();
-		const providers = currentSettings.ai.providers ?? [];
+		const providers = deepClone(currentSettings.ai.providers ?? []);
 		let updated = false;

This ensures mutations are applied to a local copy, and the final deepClone(providers) at line 76 becomes redundant (though harmless).


29-35: Unnecessary Promise.resolve wrappers.

secretStorage.getSecret and secretStorage.setSecret already return promises per Obsidian's API. The Promise.resolve() wrappers are redundant.

♻️ Suggested simplification
-				const existing = await Promise.resolve(
-					secretStorage.getSecret(secretName),
-				);
+				const existing = await secretStorage.getSecret(secretName);
 				if (!existing && legacyKey) {
-					await Promise.resolve(
-						secretStorage.setSecret(secretName, legacyKey),
-					);
+					await secretStorage.setSecret(secretName, legacyKey);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@manifest.json`:
- Line 5: The bundled artifact versions.json is out of sync with manifest.json:
update the entry for plugin version "2.9.4" in versions.json to match the new
manifest minAppVersion "1.11.4" (replace the current mapping "2.9.4": "1.6.0"
with "2.9.4": "1.11.4"); then rebuild to ensure bundled artifacts are consistent
by running the project build (e.g., bun run build-with-lint) or commit the
manual change so versions.json and manifest.json stay synchronized.
🧹 Nitpick comments (1)
src/ai/providerSecrets.ts (1)

41-82: LGTM with an optional note on loop bounds.

The implementation is solid: proper null checks, deduplication logic at line 67, and comprehensive error handling for both read and write operations (addressing the previous review feedback).

The while (true) loop lacks an explicit upper bound—in an adversarial scenario with many colliding secret names, it could iterate indefinitely. In practice this is unlikely given typical usage patterns, so it's not a blocking concern.

♻️ Optional: Add a safety bound to the loop
 const base = buildSecretBase(provider);
 let candidate = base;
 let suffix = 1;
+const MAX_SUFFIX = 1000;

-while (true) {
+while (suffix <= MAX_SUFFIX) {
   let existing: string | null = null;
   try {
     existing = await Promise.resolve(
       app.secretStorage.getSecret(candidate),
     );
   } catch (err) {
     log.logWarning(
       `Failed to read SecretStorage entry "${candidate}": ${(err as Error).message ?? err}`,
     );
   }

   if (!existing) break;
   if (existing === trimmed) return candidate;

   suffix += 1;
   candidate = `${base}-${suffix}`;
 }

+if (suffix > MAX_SUFFIX) {
+  log.logWarning(`Exceeded maximum suffix attempts for secret "${base}"`);
+  return null;
+}

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View issue and 6 additional flags in Devin Review.

Open in Devin Review

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 9 additional flags in Devin Review.

Open in Devin Review

@chhoumann chhoumann merged commit 4559013 into master Jan 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants