From 2825f83af61648deae0504eea6e55cc6d3929bae Mon Sep 17 00:00:00 2001 From: sophiamariem Date: Sat, 6 Jun 2026 15:54:58 +0100 Subject: [PATCH 1/4] inject google oauth credentials at build time --- .github/workflows/build-release.yml | 11 ++++++++ CONTRIBUTING.md | 22 +++++++++++++++- app/.env.example | 20 +++++++++++++++ app/main.js | 29 ++++++++++++++++++--- app/package-lock.json | 40 ++++++++++++----------------- app/package.json | 1 + 6 files changed, 96 insertions(+), 27 deletions(-) create mode 100644 app/.env.example diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 98c62bb5..af2c848d 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -103,6 +103,17 @@ jobs: working-directory: app run: npm ci + - name: Inject build-time secrets + # Writes app/.env (gitignored) from GitHub Actions secrets so + # electron-builder packages the credentials into app.asar at build + # time. Local dev fills its own .env from .env.example. + working-directory: app + run: | + cat > .env <=12" @@ -5204,7 +5198,6 @@ "resolved": "https://registry.npmjs.org/electron/-/electron-42.0.0.tgz", "integrity": "sha512-in5jnW/Ywy3Rh3FPr4MR80exPEkywKYAmDJRZ/gIKlr8VXEi3zXgiAZbf0Si7KRccHTF2y8euVMRz7M6HqTjMA==", "license": "MIT", - "peer": true, "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", @@ -5313,6 +5306,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -5333,6 +5327,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -5348,6 +5343,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -5358,6 +5354,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -5683,7 +5680,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8612,6 +8608,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -9302,7 +9299,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9487,6 +9483,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -9504,6 +9501,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -9715,7 +9713,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9725,7 +9722,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10107,6 +10103,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -11052,6 +11049,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -11171,7 +11169,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11386,7 +11383,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11675,7 +11671,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -11998,7 +11993,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/app/package.json b/app/package.json index 948b1a70..c4c2b778 100644 --- a/app/package.json +++ b/app/package.json @@ -82,6 +82,7 @@ "axios": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dotenv": "^16.4.5", "electron-audio-loopback": "^1.0.6", "electron-updater": "^6.8.3", "lucide-react": "^1.8.0", From 2423bb5205e297be0582a55809f6b7a43d97efab Mon Sep 17 00:00:00 2001 From: sophiamariem Date: Sat, 6 Jun 2026 16:01:07 +0100 Subject: [PATCH 2/4] address review: fail build if oauth secrets missing --- .github/workflows/build-release.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index af2c848d..011f5de5 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -107,11 +107,23 @@ jobs: # Writes app/.env (gitignored) from GitHub Actions secrets so # electron-builder packages the credentials into app.asar at build # time. Local dev fills its own .env from .env.example. + # + # Fail loudly if either secret is missing — without this guard, a + # release built before the secrets were added would silently ship + # a DMG with empty OAuth credentials, and every user's calendar + # would fail to authenticate. working-directory: app + env: + GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }} + GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }} run: | + if [ -z "${GOOGLE_OAUTH_CLIENT_ID}" ] || [ -z "${GOOGLE_OAUTH_CLIENT_SECRET}" ]; then + echo "::error::GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET must be set in repo Settings → Secrets and variables → Actions before releasing." >&2 + exit 1 + fi cat > .env < Date: Sat, 6 Jun 2026 16:05:59 +0100 Subject: [PATCH 3/4] address review: write .env via printf instead of heredoc --- .github/workflows/build-release.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 011f5de5..30103f07 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -121,10 +121,14 @@ jobs: echo "::error::GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET must be set in repo Settings → Secrets and variables → Actions before releasing." >&2 exit 1 fi - cat > .env < .env - name: Import code signing certificate uses: apple-actions/import-codesign-certs@v2 From 04d754493aecc485fb391645bc74d902f5b54ab5 Mon Sep 17 00:00:00 2001 From: sophiamariem Date: Sat, 6 Jun 2026 16:24:49 +0100 Subject: [PATCH 4/4] drop dotenv for generated build-config module --- .github/workflows/build-release.yml | 29 ++++++++++--------- .gitignore | 3 ++ CONTRIBUTING.md | 8 ++--- app/.env.example | 20 ------------- app/build-config.example.js | 22 ++++++++++++++ app/main.js | 45 +++++++++++++++-------------- app/package-lock.json | 2 +- app/package.json | 1 - 8 files changed, 69 insertions(+), 61 deletions(-) delete mode 100644 app/.env.example create mode 100644 app/build-config.example.js diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 30103f07..69afee1d 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -104,14 +104,15 @@ jobs: run: npm ci - name: Inject build-time secrets - # Writes app/.env (gitignored) from GitHub Actions secrets so - # electron-builder packages the credentials into app.asar at build - # time. Local dev fills its own .env from .env.example. + # Writes app/build-config.js (gitignored) from GitHub Actions + # secrets so electron-builder packages the credentials into the + # asar at build time. Local dev fills its own build-config.js + # from build-config.example.js. # # Fail loudly if either secret is missing — without this guard, a # release built before the secrets were added would silently ship - # a DMG with empty OAuth credentials, and every user's calendar - # would fail to authenticate. + # with empty OAuth credentials and every user's calendar would + # fail to authenticate. working-directory: app env: GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }} @@ -121,14 +122,16 @@ jobs: echo "::error::GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET must be set in repo Settings → Secrets and variables → Actions before releasing." >&2 exit 1 fi - # printf '%s\n' writes the value verbatim — no backtick / command - # substitution, no risk that a value containing the heredoc - # terminator splits the file early. Safer than a heredoc for - # writing untrusted strings to a config file. - { - printf 'GOOGLE_OAUTH_CLIENT_ID=%s\n' "${GOOGLE_OAUTH_CLIENT_ID}" - printf 'GOOGLE_OAUTH_CLIENT_SECRET=%s\n' "${GOOGLE_OAUTH_CLIENT_SECRET}" - } > .env + # Generate via node so JSON.stringify handles all escaping (quotes, + # backslashes, unicode) — safer than shell string substitution. + node -e ' + const fs = require("fs"); + const cfg = { + GOOGLE_OAUTH_CLIENT_ID: process.env.GOOGLE_OAUTH_CLIENT_ID, + GOOGLE_OAUTH_CLIENT_SECRET: process.env.GOOGLE_OAUTH_CLIENT_SECRET, + }; + fs.writeFileSync("build-config.js", "module.exports = " + JSON.stringify(cfg, null, 2) + ";\n"); + ' - name: Import code signing certificate uses: apple-actions/import-codesign-certs@v2 diff --git a/.gitignore b/.gitignore index 337f41ab..91d3bf23 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,9 @@ celerybeat.pid # Environments .env .venv +# Build-time OAuth credentials — written by CI from GitHub Actions secrets, +# or by contributors locally from build-config.example.js. Never commit. +app/build-config.js env/ venv/ ENV/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1b8ae75..4e82a357 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,13 +47,13 @@ Thank you for your interest in contributing to StenoAI! This guide will help you 5. **(Optional) Configure Google Calendar OAuth for local dev** - Calendar integration reads its OAuth credentials from `app/.env` at - build time. CI builds inject the production values from GitHub - Actions secrets; local builds need your own. + Calendar integration reads its OAuth credentials from + `app/build-config.js` at startup. CI builds write this file from + GitHub Actions secrets; local builds need your own. ```bash cd app - cp .env.example .env + cp build-config.example.js build-config.js ``` Then fill in `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET` diff --git a/app/.env.example b/app/.env.example deleted file mode 100644 index 48dd1473..00000000 --- a/app/.env.example +++ /dev/null @@ -1,20 +0,0 @@ -# Build-time secrets, loaded by app/main.js via dotenv. -# -# Copy this file to `.env` and fill in your own values for local -# development. `.env` is gitignored — never commit it. -# -# CI builds inject these from GitHub Actions secrets in -# .github/workflows/build-release.yml; the .env file is only used for -# local dev runs (`npm start` / `npm run start:nobuild`). -# -# If you leave these blank, calendar integration will be disabled but -# the rest of the app (recording, transcription, summarization) works -# normally. - -# Google Calendar OAuth credentials. Get your own from -# https://console.cloud.google.com → APIs & Services → Credentials. -# Create an OAuth 2.0 Client ID of type "Desktop application". -# Google treats Desktop-client secrets as public-by-policy; the -# production value is held in the repo's GitHub Actions secrets. -GOOGLE_OAUTH_CLIENT_ID= -GOOGLE_OAUTH_CLIENT_SECRET= diff --git a/app/build-config.example.js b/app/build-config.example.js new file mode 100644 index 00000000..8a6bc15a --- /dev/null +++ b/app/build-config.example.js @@ -0,0 +1,22 @@ +// Build-time configuration, read by app/main.js at startup. +// +// Copy this file to `build-config.js` and fill in your own values for +// local development. `build-config.js` is gitignored — never commit it. +// +// CI builds write this file directly from GitHub Actions secrets in +// .github/workflows/build-release.yml; this template is only for local +// dev runs (`npm start` / `npm run start:nobuild`). +// +// If you leave these blank, calendar integration is disabled but the +// rest of the app (recording, transcription, summarization) works +// normally. + +module.exports = { + // Google Calendar OAuth credentials. Get your own from + // https://console.cloud.google.com → APIs & Services → Credentials. + // Create an OAuth 2.0 Client ID of type "Desktop application". + // Google treats Desktop-client secrets as public-by-policy; the + // production value is held in the repo's GitHub Actions secrets. + GOOGLE_OAUTH_CLIENT_ID: '', + GOOGLE_OAUTH_CLIENT_SECRET: '', +}; diff --git a/app/main.js b/app/main.js index 50e3d848..af7915f6 100644 --- a/app/main.js +++ b/app/main.js @@ -15,21 +15,22 @@ const { PostHog } = require('posthog-node'); const { initMain } = require('electron-audio-loopback'); const { autoUpdater } = require('electron-updater'); -// Load build-time secrets from app/.env. The file is gitignored; CI writes -// it from GitHub Actions secrets before electron-builder runs (see -// .github/workflows/build-release.yml). For local dev, copy `.env.example` -// to `.env` and fill in your own Google OAuth credentials — calendar -// integration is degraded without it but the rest of the app works. +// Build-time configuration. `build-config.js` is gitignored and written +// by CI from GitHub Actions secrets before electron-builder runs (see +// .github/workflows/build-release.yml). For local dev, copy +// `build-config.example.js` to `build-config.js` and fill in your own +// Google OAuth credentials — calendar integration is "not connected" +// without it but the rest of the app works. // -// `silent: true` so a missing `.env` doesn't spam the console for -// contributors who haven't set up calendar integration. -require('dotenv').config({ - path: path.join(__dirname, '.env'), - // dotenv v17+ accepts `quiet`; older `silent` is harmless. Use both - // so we don't have to pin to a specific minor. - quiet: true, - silent: true, -}); +// Plain require + try/catch (rather than dotenv) so we don't ship a +// runtime dependency just to read two constants. +let buildConfig = {}; +try { + buildConfig = require('./build-config'); +} catch (_) { + // Missing file in local dev → calendar surfaces as "not connected" + // rather than crashing. +} // E2E test-harness hooks. Set via env vars; production sees none of these. // STENOAI_USER_DATA_DIR — per-test temp userData dir (must be set before app.whenReady) @@ -374,14 +375,14 @@ const POSTHOG_HOST = 'https://us.i.posthog.com'; // Google Calendar OAuth2 configuration. // -// Both values are injected at build time via dotenv (see top of file). -// Empty strings in local dev with no `.env` mean Google auth is disabled -// — the renderer surfaces this as "calendar not connected" rather than a -// crash. Rotate the secret in Google Cloud Console + update the -// `GOOGLE_OAUTH_CLIENT_*` GitHub Actions secrets to roll a new -// credential without a code change. -const GOOGLE_CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID || ''; -const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET || ''; +// Both values come from `build-config.js` (see top of file). Empty +// strings in local dev with no `build-config.js` mean Google auth is +// disabled — the renderer surfaces this as "calendar not connected" +// rather than a crash. Rotate the secret in Google Cloud Console + +// update the `GOOGLE_OAUTH_CLIENT_*` GitHub Actions secrets to roll a +// new credential without a code change. +const GOOGLE_CLIENT_ID = buildConfig.GOOGLE_OAUTH_CLIENT_ID || ''; +const GOOGLE_CLIENT_SECRET = buildConfig.GOOGLE_OAUTH_CLIENT_SECRET || ''; const GOOGLE_SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'; const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'; diff --git a/app/package-lock.json b/app/package-lock.json index c2eb9863..4df23512 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -21,7 +21,6 @@ "axios": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dotenv": "^16.4.5", "electron-audio-loopback": "^1.0.6", "electron-updater": "^6.8.3", "lucide-react": "^1.8.0", @@ -5139,6 +5138,7 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/app/package.json b/app/package.json index c4c2b778..948b1a70 100644 --- a/app/package.json +++ b/app/package.json @@ -82,7 +82,6 @@ "axios": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dotenv": "^16.4.5", "electron-audio-loopback": "^1.0.6", "electron-updater": "^6.8.3", "lucide-react": "^1.8.0",