Skip to content

feat(user-scripts): store secret settings in SecretStorage#1373

Merged
chhoumann merged 7 commits into
masterfrom
chhoumann/user-script-secrets-vault
Jun 17, 2026
Merged

feat(user-scripts): store secret settings in SecretStorage#1373
chhoumann merged 7 commits into
masterfrom
chhoumann/user-script-secrets-vault

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add type: "secret" user-script settings backed by Obsidian app.secretStorage, with legacy type: "text" | "input", secret: true treated the same way.
  • Resolve secret markers into ephemeral runtime settings for user scripts, without writing plaintext back to data.json.
  • Lazily migrate existing plaintext user-script secret settings into SecretStorage when script settings are known; if SecretStorage is unavailable or write fails, leave plaintext untouched to avoid data loss.
  • Mask secret inputs in the user-script settings modal with explicit save/clear actions, and clear local SecretStorage refs when commands/choices are deleted.
  • Strip local secret refs and detected legacy plaintext secret settings from package export/import and macro duplication paths.
  • Update user-script docs for secret settings and local-only package behavior.

Design notes

  • Secret IDs are namespaced per command and setting (quickadd-user-script-<command>-<setting>) and persisted in settings as { __quickaddSecret: true, secretRef } markers.
  • Runtime resolution returns a copied settings object so command settings stay marker-only after migration.
  • Migration is idempotent and only replaces plaintext after a successful SecretStorage write.
  • Package export/import inspects bundled user-script source for secret option declarations, including quoted property names, and strips matching legacy plaintext values. Dynamic/unknown secret option names fall back conservatively.
  • Macro duplication uses the current vault to inspect referenced user scripts before copying, preserving normal string settings while removing detected secret values.

Issue tracker check

Validation

  • pnpm exec tsc --noEmit --skipLibCheck --pretty false
  • pnpm run lint
  • pnpm run check (0 errors, existing 4 warnings)
  • pnpm run test (218 files passed, 2533 tests passed, 31 skipped)
  • pnpm run build

Real Obsidian E2E evidence

Used this worktree's isolated vault via pnpm run obsidian:e2e -- ....

  • Set a secret through the real QuickAdd settings UI and UserScriptSettingsModal password input.
  • Confirmed UI affordances: password input, Save API Key, Clear API Key.
  • Confirmed SecretStorage contained qa-e2e-secret-value-173 while data.json contained only the marker and did not contain plaintext.
  • Ran quickadd:run choice='Secret Roundtrip'; user script read the resolved secret and wrote it to secret-output.txt, while data.json still had no plaintext.
  • Updated the secret through the UI, reran the macro, and confirmed the script received the updated value without plaintext persistence.
  • Cleared the secret through the UI, confirmed settings no longer contained API Key, SecretStorage returned null, runtime output was empty, and data.json contained neither old nor updated plaintext.
  • Seeded legacy plaintext in data.json, reloaded QuickAdd, ran the macro, and confirmed lazy migration wrote SecretStorage, persisted the marker, removed plaintext from data.json, and passed the secret to the script.

Release / migration impact

  • New feature for user-script settings.
  • Existing plaintext user-script secrets are migrated lazily and safely when SecretStorage is available. On old/mobile builds without SecretStorage, QuickAdd leaves plaintext untouched and warns instead of dropping data.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added first-class secret option type for user scripts with password-style input and “API Key” example.
  • Improvements

    • Secrets are stored locally in the app profile, referenced safely during use, and omitted from exports/sync.
    • Automatic migration from legacy plaintext secrets to secure storage during execution, duplication/deletion, and package import/export.
    • Secret clearing now fails safely with clear notices when removal isn’t possible.
  • Documentation

    • Updated advanced and user script docs with secret guidance and backward compatibility.
  • Tests

    • Expanded coverage for migration, sanitization, UI, and package behaviors.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23ea3c8a-899f-48e7-9a0d-9c5e4db929ca

📥 Commits

Reviewing files that changed from the base of the PR and between f322338 and 9e1e687.

📒 Files selected for processing (2)
  • src/services/choiceService.ts
  • tests/obsidian-stub.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/obsidian-stub.ts
  • src/services/choiceService.ts

📝 Walkthrough

Walkthrough

Adds end-to-end support for a type: "secret" user-script setting backed by Obsidian SecretStorage. A new utility module (userScriptSecrets.ts) handles script parsing, secret storage, runtime resolution, legacy migration, and sanitization. Secret handling is integrated into macro engines, the settings modal UI, command/choice deletion, choice duplication, and the package import/export pipeline. Documentation and tests are included throughout.

Changes

User-Script Secret Settings

Layer / File(s) Summary
Core secret utility: types, parsing, storage, resolution, sanitization
src/utils/userScriptSecrets.ts, src/utils/userScriptSecrets.test.ts
New module exports types (UserScriptOptionDefinition, UserScriptSecretRef, etc.) and all secret functions: JS/TS source parser detecting type: "secret" options, deterministic secret-ID builder, SecretStorage read/write helpers, settings resolution replacing secret-ref fields with stored plaintext, legacy-migration function, recursive clearing across command trees, and secret-ref stripping with optional unknown-setting sanitization. Full Vitest suite covers all behaviors.
Secret exclusion from default settings initialization
src/utils/userScriptSettings.ts
initializeUserScriptSettings now skips assigning defaultValue for options classified as secrets, preventing plaintext defaults from being written to command settings.
UserScriptSettingsModal: secret input UI, migration, save/clear
src/gui/MacroGUIs/UserScriptSettingsModal.ts, src/gui/MacroGUIs/UserScriptSettingsModal.test.ts, tests/obsidian-stub.ts, tests/vitest-setup.ts
UserScriptSettingsModal gains a type: "secret" variant in the Option union and a new addSecretInput method rendering a password field with buffered pending value, dynamic placeholder, and async Save/Clear actions. Constructor triggers async migrateSecretSettings. The Modal stub gains DOM element construction and onOpen/onClose hooks; jsdom polyfills for addClass, removeClass, empty, createDiv, createEl are added. Tests cover save, migration-on-open, and clear.
Engine: secret migration and resolution before script invocation
src/engine/MacroChoiceEngine.ts, src/engine/SingleMacroEngine.ts, src/engine/MacroChoiceEngine.entry.test.ts, src/engine/SingleMacroEngine.member-access.test.ts
MacroChoiceEngine stores the executing command and settings definition, migrates legacy secrets and persists via saveSettings, then passes resolved settings to both entry and direct-function export paths. SingleMacroEngine migrates and resolves for callable member-access exports while non-callable member access continues using raw settings. Engine tests verify migration, storage writes, and correct invocation arguments.
Command deletion and choice duplication: secret clearing and sanitization
src/gui/MacroGUIs/CommandSequenceEditor.ts, src/services/choiceService.ts, src/gui/choiceList/ChoiceView.svelte, src/services/choiceService.test.ts
CommandSequenceEditor clears secrets before deleting a command, showing a Notice and aborting on failure. choiceService gates Macro/Multi choice deletion on successful secret clearing and adds duplicateChoiceWithUserScriptSecretSanitization that inspects referenced script files to build secretOptionNamesByPath and strips secret refs from duplicated choice trees. ChoiceView.svelte switches to the sanitization-aware duplicate. Tests verify duplication sanitization and deletion success/failure paths.
Package export and import: secret-ref stripping pipeline
src/services/packageExportService.ts, src/services/packageImportService.ts, src/services/packageExportService.test.ts, src/services/packageImportService.test.ts
packageExportService adds buildSecretOptionNamesByPath and strips secret refs from all included choices before serialization. packageImportService computes secretOptionNamesByPath from incoming assets and propagates secretSanitizerOptions through all recursive remapping paths. Tests assert secret refs and legacy secret fields are absent from exported and imported output.
Documentation: secret setting type and examples
docs/docs/Advanced/scriptsWithSettings.md, docs/docs/UserScripts.md
Docs updated to document type: "secret", SecretStorage behavior, export/sync omission, backward compatibility with secret: true, and updated complete-example snippets using the new form.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(135, 180, 250, 0.5)
    Note over UserScriptSettingsModal: Modal Open
    UserScriptSettingsModal->>migrateUserScriptSecretSettings: app, command, settingsDef
    migrateUserScriptSecretSettings->>SecretStorage: store legacy plaintext secret
    migrateUserScriptSecretSettings-->>UserScriptSettingsModal: changed=true
    UserScriptSettingsModal->>UserScriptSettingsModal: onCommandChange() + display()
  end
  rect rgba(80, 200, 140, 0.5)
    Note over UserScriptSettingsModal: Save Secret
    UserScriptSettingsModal->>storeUserScriptSecret: app, command, settingName, value
    storeUserScriptSecret->>SecretStorage: secretStorage.store(secretId, value)
    storeUserScriptSecret-->>UserScriptSettingsModal: secretRef
    UserScriptSettingsModal->>UserScriptSettingsModal: command.settings[name] = createUserScriptSecretRef(secretRef)
  end
  rect rgba(250, 160, 100, 0.5)
    Note over MacroChoiceEngine: Script Execution
    MacroChoiceEngine->>migrateUserScriptSecretSettings: app, command, settingsDef
    migrateUserScriptSecretSettings-->>MacroChoiceEngine: changed → saveSettings()
    MacroChoiceEngine->>resolveUserScriptSettings: app, command, settingsDef
    resolveUserScriptSettings->>SecretStorage: read secret by secretRef
    resolveUserScriptSettings-->>MacroChoiceEngine: { secretName: plaintextValue }
    MacroChoiceEngine->>UserScript: invoke with resolved settings
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 Hoppity hop, secrets stay safe now,
No API keys exposed in a JSON file,
SecretStorage guards them — hidden from view,
Migration runs quietly, smooth as dew,
The rabbit says: "Your tokens? Secure! 🔑"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(user-scripts): store secret settings in SecretStorage' directly and clearly describes the main change: implementing storage of secret user-script settings in Obsidian's SecretStorage API.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chhoumann/user-script-secrets-vault

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9e1e687
Status: ✅  Deploy successful!
Preview URL: https://b7d3f760.quickadd.pages.dev
Branch Preview URL: https://chhoumann-user-script-secret.quickadd.pages.dev

View logs

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

Copy link
Copy Markdown

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: f14a0f6a79

ℹ️ 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".

Comment thread src/utils/userScriptSecrets.ts
Comment thread src/utils/userScriptSecrets.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/obsidian-stub.ts (1)

519-544: ⚡ Quick win

Use tab indentation in the new Modal stub block.

This block is indented with spaces, but this repository’s TS style requires tabs.

As per coding guidelines, **/*.{ts,tsx,js,jsx,svelte,css,scss,json,md} must use tab indentation and LF line endings as configured in .editorconfig.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/obsidian-stub.ts` around lines 519 - 544, Replace all space indentation
with tab indentation throughout the Modal class block (including the
constructor, open method, and close method). Ensure all lines within the class
definition, from the property declarations through the close method
implementation, use tabs instead of spaces to comply with the repository's
.editorconfig settings that require tab indentation for TypeScript files.

Source: Coding guidelines

src/services/choiceService.ts (1)

236-254: 💤 Low value

Consider consolidating the duplicated secret-clearing branches.

The isMacro and isMulti branches execute identical code. Combining them into a single condition improves maintainability.

♻️ Suggested consolidation
-	if (isMacro) {
-		const cleared = await clearUserScriptSecretsFromCommand(app, {
-			type: "NestedChoice",
-			choice,
-		});
-		if (!cleared) {
-			new Notice("Could not clear user script secrets. Choice was not deleted.");
-			return false;
-		}
-	} else if (isMulti) {
-		const cleared = await clearUserScriptSecretsFromCommand(app, {
-			type: "NestedChoice",
-			choice,
-		});
-		if (!cleared) {
-			new Notice("Could not clear user script secrets. Choice was not deleted.");
-			return false;
-		}
-	}
+	if (isMacro || isMulti) {
+		const cleared = await clearUserScriptSecretsFromCommand(app, {
+			type: "NestedChoice",
+			choice,
+		});
+		if (!cleared) {
+			new Notice("Could not clear user script secrets. Choice was not deleted.");
+			return false;
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/choiceService.ts` around lines 236 - 254, The isMacro and
isMulti branches contain identical code that calls
clearUserScriptSecretsFromCommand with the same parameters and handles errors
identically. Consolidate these two branches into a single conditional statement
by combining the conditions with OR logic (isMacro || isMulti), and remove the
duplicate code block so the secret-clearing logic and error handling only
appears once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/services/choiceService.ts`:
- Around line 236-254: The isMacro and isMulti branches contain identical code
that calls clearUserScriptSecretsFromCommand with the same parameters and
handles errors identically. Consolidate these two branches into a single
conditional statement by combining the conditions with OR logic (isMacro ||
isMulti), and remove the duplicate code block so the secret-clearing logic and
error handling only appears once.

In `@tests/obsidian-stub.ts`:
- Around line 519-544: Replace all space indentation with tab indentation
throughout the Modal class block (including the constructor, open method, and
close method). Ensure all lines within the class definition, from the property
declarations through the close method implementation, use tabs instead of spaces
to comply with the repository's .editorconfig settings that require tab
indentation for TypeScript files.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 340f00e1-9650-4fe9-92d0-82d6a239fad8

📥 Commits

Reviewing files that changed from the base of the PR and between 3d97a16 and f14a0f6.

📒 Files selected for processing (21)
  • docs/docs/Advanced/scriptsWithSettings.md
  • docs/docs/UserScripts.md
  • src/engine/MacroChoiceEngine.entry.test.ts
  • src/engine/MacroChoiceEngine.ts
  • src/engine/SingleMacroEngine.member-access.test.ts
  • src/engine/SingleMacroEngine.ts
  • src/gui/MacroGUIs/CommandSequenceEditor.ts
  • src/gui/MacroGUIs/UserScriptSettingsModal.test.ts
  • src/gui/MacroGUIs/UserScriptSettingsModal.ts
  • src/gui/choiceList/ChoiceView.svelte
  • src/services/choiceService.test.ts
  • src/services/choiceService.ts
  • src/services/packageExportService.test.ts
  • src/services/packageExportService.ts
  • src/services/packageImportService.test.ts
  • src/services/packageImportService.ts
  • src/utils/userScriptSecrets.test.ts
  • src/utils/userScriptSecrets.ts
  • src/utils/userScriptSettings.ts
  • tests/obsidian-stub.ts
  • tests/vitest-setup.ts

@chhoumann

Copy link
Copy Markdown
Owner Author

Addressed the remaining CodeRabbit review-body nitpicks in 9e1e687: consolidated the duplicated Macro/Multi secret-clearing branch in choiceService.ts, and converted the new Modal test stub block in tests/obsidian-stub.ts to tab indentation. Validation after the cleanup: pnpm exec vitest run src/services/choiceService.test.ts src/gui/MacroGUIs/UserScriptSettingsModal.test.ts --maxWorkers=1, pnpm exec tsc --noEmit --skipLibCheck --pretty false, pnpm run lint, and git diff --check.

@chhoumann chhoumann merged commit 288a1b5 into master Jun 17, 2026
10 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.

1 participant