Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7120f7e
NPM Updates
acrosman Mar 9, 2026
dc143b6
Merge branch 'main' into features/issue-55-oauth
acrosman Mar 9, 2026
b148a1a
Step one: add settings
acrosman Mar 9, 2026
35dc147
Step 2 Add oauth handler.
acrosman Mar 9, 2026
37bed64
Step 3: oauth support in main
acrosman Mar 9, 2026
807c5a1
Re-Tighten security on navigate events.
acrosman Mar 9, 2026
1cbb309
Step 4 update UI
acrosman Mar 9, 2026
7015131
Step 5: Update tests
acrosman Mar 9, 2026
33e58f4
Step 6 Update ReadMe
acrosman Mar 9, 2026
0376c89
Clean up task 1: Update browser handling and tests
acrosman Mar 9, 2026
7eb1bf1
Clean up prompt 2: render.js clean up
acrosman Mar 9, 2026
be9009e
Clean up task 3: Make callback port a setting.
acrosman Mar 9, 2026
486d543
Clean up task 4: Refresh token support
acrosman Mar 9, 2026
c7813b9
Clean up task 5: Guards for missing keys
acrosman Mar 9, 2026
c041df8
Update instructions file
acrosman Mar 9, 2026
21b62d7
Add test workflow.
acrosman Mar 9, 2026
9f32648
Initial plan
Copilot Mar 9, 2026
c80acf5
Apply security and bug fixes from PR review feedback
Copilot Mar 9, 2026
9411adc
Merge pull request #107 from acrosman/copilot/sub-pr-106
acrosman Mar 9, 2026
fdf918a
general package updates again
acrosman Mar 9, 2026
cb28516
Electron 40
acrosman Mar 9, 2026
a3eff36
Fix bug when establishing connections.
acrosman Mar 10, 2026
a5677dc
Edits to setup instrunctions
acrosman Mar 10, 2026
847a794
Fix login UX
acrosman Mar 10, 2026
7b79b0c
Setup SafeStorage for encryption of keys,
acrosman Mar 10, 2026
b7b937b
Linter fixes
acrosman Mar 10, 2026
5e8dc22
Update documentation images.
acrosman Mar 10, 2026
bed5052
Fixes to codeql workflow.
acrosman Mar 10, 2026
194accb
CodeQL v4
acrosman Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
.vscode
.sfdx
out
coverage
38 changes: 23 additions & 15 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
module.exports = {
"env": {
"browser": true,
"es6": true
env: {
browser: true,
es6: true,
},
extends: 'airbnb-base',
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaVersion: 2020,
},
rules: {
'no-unused-vars': 1,
'no-param-reassign': ['error', { props: false }],
},
overrides: [
{
files: ['tests/**/*.js', '__mocks__/**/*.js'],
env: {
jest: true,
},
},
"extends": "airbnb-base",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"no-unused-vars": 1,
"no-param-reassign": ["error", { "props": false }]
}
],
};
21 changes: 18 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ ElectronForce is an [Electron](https://www.electronjs.org/)-based desktop applic
electronForce/
├── main.js # Electron main process: window creation, app lifecycle, IPC handler registration
├── src/
│ └── electronForce.js # Core backend logic: JSForce connection management and IPC handler definitions
│ ├── electronForce.js # Core backend logic: JSForce connection management and IPC handler definitions
│ └── settings.js # Read/write persistent user settings via app.getPath('userData')/settings.json
├── __mocks__/
│ └── electron.js # Jest manual mock for the electron module (shared across all test files)
├── app/
│ ├── index.html # Application UI markup
│ ├── render.js # Renderer process logic: UI interaction and IPC send/receive calls
Expand All @@ -21,6 +24,11 @@ electronForce/
├── .github/
│ ├── workflows/ # CI workflows: lint.yml and codeql-analysis.yml
│ └── ISSUE_TEMPLATE/ # Bug report and feature request templates
├── tests/
│ ├── electronForce.test.js # Tests for src/electronForce.js IPC handlers
│ ├── handlers.test.js # Tests for individual handler logic
│ ├── render.test.js # Tests for app/render.js renderer logic
│ └── settings.test.js # Tests for src/settings.js read/write logic
├── .husky/
│ └── pre-commit # Runs lint and tests before every commit
├── .eslintrc.js # ESLint configuration (airbnb-base)
Expand All @@ -39,10 +47,15 @@ ElectronForce follows the standard Electron two-process model with strict securi

### IPC Channel Convention

- Channels sent **from the renderer to the main process** use the prefix `sf_` (e.g., `sf_login`, `sf_query`) or `get_` (e.g., `get_log_messages`).
- Channels sent **from the main process back to the renderer** use the prefix `response_` (e.g., `response_login`, `response_query`, `response_generic`).
- Channels sent **from the renderer to the main process** use the prefix `sf_` (e.g., `sf_oauth_start`, `sf_query`) or `get_` (e.g., `get_log_messages`). Note: `sf_oauth_start` is the current authentication entry point; `sf_login` is no longer used.
- Settings persistence uses `sf_get_settings` and `sf_save_settings`; the main process responds on `response_settings`.
- Channels sent **from the main process back to the renderer** use the prefix `response_` (e.g., `response_query`, `response_generic`, `response_settings`).
- Every new handler added to `src/electronForce.js` must also be added to the `validChannels` allow-list in `app/preload.js`.

### Authentication

Authentication uses Salesforce OAuth 2.0 via a Salesforce External Client App. The main process opens the authorization URL in the system browser via `shell.openExternal`. A one-time local HTTP server (default port 3835, configurable in settings) receives the callback, exchanges the auth code for tokens using `jsforce.OAuth2`, and stores the resulting connection in `sfConnections`. The local server closes after a successful exchange or a 5-minute timeout.

### Response Payload Shape

All IPC responses follow a consistent structure:
Expand All @@ -67,6 +80,7 @@ This project follows the guidelines described in `contributing.md`. Key points:
- **Security**: Never enable `nodeIntegration`, `enableRemoteModule`, or disable `contextIsolation` in `BrowserWindow` settings. Keep the preload channel allow-lists up to date.
- **No unused variables**: The ESLint rule `no-unused-vars` is set to `warn`. Treat warnings as errors before opening a pull request.
- **Parameter reassignment**: Direct property mutations on function parameters are allowed (`"props": false`), but avoid reassigning the parameter binding itself.
- **Settings mock**: Any test file that imports or indirectly loads `src/electronForce.js` must include `jest.mock('../src/settings')` to prevent real file-system reads/writes during testing.

## Testing

Expand All @@ -75,6 +89,7 @@ Tests are written with [Jest](https://jestjs.io/) and live alongside the source
- After any session that modifies code beyond comments, ensure the full test suite passes and linting is clean before considering the work done.
- When adding new IPC handlers or utility functions, add corresponding Jest tests.
- Tests for the main-process logic in `src/electronForce.js` should mock `jsforce` and the `mainWindow` object to avoid requiring a live Electron or Salesforce environment.
- Any test file that needs Electron APIs (e.g., `app.getPath`) should call `jest.mock('electron')` (no factory). Jest will automatically use the shared manual mock at `__mocks__/electron.js`, which returns a temp-directory path for `app.getPath('userData')` and exposes `shell.openExternal` as a `jest.fn()` so tests can assert on it without triggering real browser calls. Do not duplicate the mock factory inline in individual test files.
- The `--passWithNoTests` flag is used for the pre-commit hook so that new files without tests don't block commits, but coverage is expected for substantive logic.

## Contribution Expectations
Expand Down
58 changes: 29 additions & 29 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ name: "CodeQL"

on:
push:
branches: [ main ]
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '41 8 * * 4'
- cron: "41 8 * * 4"

jobs:
analyze:
Expand All @@ -32,40 +32,40 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
language: ["javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed

steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4

# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl

# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language

#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
28 changes: 28 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Tests

on:
push:
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]

jobs:
test:
name: Tests
runs-on: ubuntu-latest

steps:
- name: Check out Git repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test
30 changes: 25 additions & 5 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,37 @@ From your terminal:
npm install
npm start

ElectronForce will allow you to log into your Salesforce Org and interact with some of the APIs. While all of JSForce's supported APIs are listed, only Query, Search, and Describe are currently support.
ElectronForce uses OAuth to connect to your Salesforce Org and interact with the APIs. While all of JSForce's supported APIs are listed, only Query, Search, and Describe are currently supported.

### Log in
### Setup: Create a Salesforce External Client App

Currently only the standard login is supported, not OAuth2, so you likely will need your [security token](https://help.salesforce.com/articleView?id=user_security_token.htm&type=5).
Before connecting ElectronForce to a Salesforce org you need to create an External Client App to generate OAuth credentials:

In the login fields provide your username, password, and security token. If you are logging into a production or trailhead instance you can use the default login URL. If you are logging into a Sandbox use: https://test.salesforce.com.
1. In Salesforce, navigate to **Setup → App Manager** (use Quick Find if needed).
2. Click **New External Client App**.
3. Enter a **Name** and accept or edit the generated **API Name**.
4. Enter a **Contact Email**.
5. Set the **Distribution State** to **Local** (for use only in this org).
6. Save the app, then edit it and enable **OAuth**.
7. In the OAuth settings, set the **Callback URL** to `http://localhost:3835/callback`.
8. Add the following **OAuth Scopes**: `api` and `refresh_token`.
9. Save. Copy the **Consumer Key** (Client ID) and **Consumer Secret** from the OAuth detail page.

For full details see the Salesforce Help article [Create an External Client App](https://help.salesforce.com/s/articleView?id=xcloud.create_a_local_external_client_app.htm&type=5).

### Configure ElectronForce

Open the **Settings** modal in ElectronForce and enter the **Consumer Key** and **Consumer Secret** from your External Client App. If you are connecting to a sandbox org, set the **Login URL** to `https://test.salesforce.com`. For production and Trailhead orgs the default login URL (`https://login.salesforce.com`) can be used.

> **Security note:** The Consumer Key and Consumer Secret are encrypted at rest using [`electron.safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage), which delegates to the OS-level credential store (Keychain on macOS, the secret service on Linux, DPAPI on Windows). On platforms where OS-level encryption is unavailable, the values fall back to plain text in the application's user-data directory.

### Connect to Salesforce

Click the **Connect via OAuth** button. ElectronForce will open the Salesforce login page in your default browser. After you approve access, Salesforce redirects to the local callback URL and ElectronForce completes the OAuth handshake automatically.

![ElectronForce Main screen.](./documentation/images/ElectronForceMain.png "Login fields as described above and query API example as follows.")

The main interface includes the login information, API selector and parameter fields on the left, raw display of the previous API response on the right, and a processed version of the response at the bottom.
The main interface includes the API selector and parameter fields on the left, raw display of the previous API response on the right, and a processed version of the response at the bottom.

### Run Query or Search

Expand Down
18 changes: 18 additions & 0 deletions __mocks__/electron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const os = require('os');
const path = require('path');

module.exports = {
app: {
getPath: jest.fn(() => path.join(os.tmpdir(), 'electronforce-test')),
},
shell: {
openExternal: jest.fn(),
},
safeStorage: {
isEncryptionAvailable: jest.fn(() => true),
// Passthrough: encode the string into a Buffer so round-trips work in tests.
encryptString: jest.fn((str) => Buffer.from(str, 'utf8')),
// Passthrough: decode the Buffer back to the original string.
decryptString: jest.fn((buf) => buf.toString('utf8')),
},
};
Loading