secrets

Source: secrets.js:21

Encrypted secrets store for OAuth tokens and API keys. Secrets NEVER go in settings.json; they live here, encrypted at rest via Electron's safeStorage, whose master key is protected by the OS credential store (macOS Keychain, Windows Credential Manager / DPAPI, Linux libsecret / kwallet when available).

Only the ciphertext (base64) is held in a file alongside settings, at app.getPath('userData')/secrets.json; safeStorage holds the key. So the file by itself is useless to anyone who copies it off the disk — defending against offline file theft and backup leakage. It does NOT defend against code already running as this user (that code can ask safeStorage to decrypt); that's the standard tradeoff of OS-backed app secret storage.

Security posture: main process only. The renderer is deliberately given NO way to read or write plaintext secrets (see main.js: only availability/has/delete are exposed over IPC). In the real OAuth flow (D8/D9) the token is obtained and stored entirely in the main process, so plaintext never crosses into the renderer.


Instance Methods

filePath(): string

Absolute path of secrets.json in the Electron userData directory.

Returns

  • string — File path.

isAvailable(): boolean

Whether safeStorage can actually encrypt. On Linux with no keyring backend this is false; callers must check before set() and degrade gracefully (e.g. "secure storage unavailable — can't connect this account") rather than storing plaintext.

Returns

  • boolean — True when encryption is available.

load(): Object.<string, string>

Read the ciphertext map from disk once, then serve from memory. Missing file = no secrets yet; an unreadable/corrupt file is logged and treated as empty so a bad file can't brick launch (the secrets it held are lost, which is acceptable — the user simply re-connects the affected account).

Returns

  • Object.<string, string> — The ciphertext map.

persist(data: Object.<string, string>): void

Atomic write with owner-only permissions (0o600). Even though the contents are already ciphertext, restricting the file mode is cheap defense-in-depth. Temp-file + rename means a crash mid-write can't leave a half-written secrets.json.

Parameters

  • data (Object.<string, string>) — Ciphertext map to persist.

Returns

  • void

setSecret(key: string, value: string): void

Encrypt value and persist it under key. Throws if secure storage isn't available — falling back to plaintext is refused.

Parameters

  • key (string) — Secret name, e.g. "frameio.accessToken".
  • value (string) — Plaintext secret to encrypt and store.

Returns

  • void

Throws

  • Error — When secure storage is unavailable or value is not a string.

getSecret(key: string): string

Decrypt and return the secret, or null if no secret is stored under key. Throws only when a blob exists but can't be decrypted (storage went unavailable, or key mismatch). Main-process use only — never wire this to the renderer.

Parameters

  • key (string) — Secret name.

Returns

  • string — Plaintext secret, or null when absent.

Throws

  • Error — When a blob exists but secure storage can't decrypt it.

hasSecret(key: string): boolean

Whether a secret is stored under key. Cheap boolean — no decryption, so it works even when safeStorage is unavailable. This is what the renderer uses to show connected state.

Parameters

  • key (string) — Secret name.

Returns

  • boolean — True when a blob exists for key.

deleteSecret(key: string): boolean

Remove a secret (account disconnect). No-op if absent.

Parameters

  • key (string) — Secret name.

Returns

  • boolean — True if something was removed.

listKeys(): Array.<string>

The keys currently held (no values). Handy for diagnostics / "which accounts are linked".

Returns

  • Array.<string> — Stored secret names.

Instance Fields

cache: Object.<string, string>

In-memory map of key → base64 ciphertext — encrypted blobs, never plaintext. Null until first load.