Close critical production-readiness items (C-1, C-3, C-6, C-8, C-9, C-10, C-12) + external action checklist#114
Merged
Conversation
The top-level functions/ directory contained Deno-targeted .ts files that predated the migration to Node + Electron. The live runtime code lives in electron/functions/ and nothing in the application graph imports the top-level folder; this commit removes 16 dead files (~3,000 LOC) and fixes one stale URL reference in EHRIntegrationManager.jsx that pointed at the old path. Risk: none. Confirmed via repo-wide grep that no module resolves against the top-level functions/ path. The electron/functions/ sibling that the production code actually uses is untouched. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a --for-sale flag to the release-readiness gate that promotes the
Windows code-signing, macOS notarization, and signed-installer presence
checks from `optional` to `mandatory`. Without the flag, day-to-day
`npm run release:check` keeps the same ergonomics it has today.
Also adds .github/workflows/release.yml that triggers on v*.*.* tags
and:
- preflights the signing-secret environment (refuses to start if
Windows or Apple credentials are missing)
- builds, signs and notarizes Windows + macOS installers in parallel
- runs `npm run release:check:for-sale` as the commercial gate
- publishes signed artifacts to the GitHub Releases page
Net effect: it is no longer possible to ship a binary to a customer
through the normal release path without code-signing credentials
present in CI. docs/CODE_SIGNING.md documents the workflow.
No effect on existing builds — the CI workflow only activates on a
tag push, and the new gate flag only applies when explicitly passed.
Co-authored-by: Cursor <cursoragent@cursor.com>
… (C-10)
Adds a server-side billing surface so customer purchases auto-emit
signed TransTrack license files instead of requiring the founder to
run `npm run license:issue` manually for every sale.
Endpoints (both public; webhook is signature-verified):
POST /v1/billing/checkout-session
Creates a Stripe Checkout Session with PKCE-style metadata
(tier, orgId, customerName, customerEmail, optional machineIds)
and returns the hosted-checkout URL.
POST /v1/billing/webhook
Verifies the Stripe-Signature header against
STRIPE_WEBHOOK_SECRET. On checkout.session.completed:
1. Builds an Ed25519-signed license using the issuance helper
that backs `scripts/issue-license.mjs`
2. Persists to a new `issued_licenses` table
3. Emails the .lic file to the customer via SMTP (best-effort;
falls back to operator-visible log line if SMTP isn't set)
On invoice.paid: stub for renewal re-issuance (TODO).
On customer.subscription.deleted: marks the row canceled.
Infra:
- server/src/index.js: per-route raw-body capture hook so Stripe
signature verification has access to the unparsed body.
- server/src/config.js: STRIPE_*, LICENSE_PRIVATE_KEY_PATH, SMTP_*
config entries, all optional — server still boots without them.
- server/package.json: stripe + nodemailer listed under
optionalDependencies so existing pilot installs are unaffected.
- server/src/db/migrations/006_issued_licenses.sql: audit + renewal
tracking table indexed by customer_email, org_id, stripe
subscription_id, and expires_at.
- docs/STRIPE_BILLING.md: operator's guide (vendor setup, secrets,
smoke-test instructions, security caveats).
If Stripe credentials are not configured, both endpoints return 503,
so this commit is safe to ship to existing pilot deployments.
Co-authored-by: Cursor <cursoragent@cursor.com>
…-token hardening
Closes critical-tier items C-1, C-6, C-8, and C-9 from the production
readiness audit. These four are grouped into one commit because they
share several touched files (auth.cjs, entities.cjs, preload.cjs,
migrations.cjs, package.json, localClient.js) and splitting them at
the file level would create commits that do not compile in isolation.
================================================================
C-1: Ed25519 signed-license activation system
================================================================
Replaces the stubbed electron/license/manager.cjs (which previously
hard-coded "fully licensed") with a real implementation:
electron/license/
machineId.cjs stable per-install fingerprint + HKDF
binding hash; resists casual key-sharing
publisherPublicKey.cjs embedded publisher pubkey (env-overridable
at build time for production rotation),
carries LICENSE_PROTOCOL_VERSION
issuance.cjs LIC1.* wire format, Ed25519 sign + verify,
strict schema validation
verifier.cjs orchestrates: signature -> protocol version
-> expiry (with 14-day soft-expiry grace)
-> machine-binding check
storage.cjs license file at userData/license.dat,
0o600; 30-day trial state machine that
does NOT reset on reinstall
manager.cjs public API (preserves the surface the
rest of the app already calls): trial,
trial_expired, active, in_grace, invalid
Issuance + activation surface:
scripts/license-keypair.mjs one-time publisher keypair gen
scripts/issue-license.mjs CLI to sign a customer license
electron/ipc/handlers/license.cjs IPC: getInfo, getMachineId,
activate, remove, checkFeature,
checkLimit (admin-gated mutators)
electron/preload.cjs renderer bridge under
window.electronAPI.license
src/api/localClient.js api.license.* + browser-dev mock
src/pages/License.jsx full activation UI: machine ID
copy, license paste-and-activate,
remove, trial countdown banner
src/pages.config.js + Sidebar.jsx wires the new admin page
Enforcement:
electron/ipc/handlers/entities.cjs entity:create now consults the
manager on Patient and User
creation; refuses past the
licensed cap; reverts to read-
only after trial_expired.
Docs + tests:
docs/LICENSING.md operator guide (issuance, rotation,
error codes, threat model caveats)
tests/license.test.cjs 20 tests: sign/verify, tampering,
expiry, in-grace, machine binding,
activation persistence, trial lifecycle,
limit enforcement, feature gating
.gitignore excludes keys/ so the Ed25519 private
key generated by the keypair script
cannot be committed
================================================================
C-6: Field-level encryption of EHR API keys
================================================================
The ehr_integrations.api_key_encrypted column previously stored raw
plaintext credentials despite its name. Replaced with AES-256-GCM
field encryption:
electron/services/secretEncryption.cjs
HKDF-SHA256 subkeys per column from a 32-byte master persisted
under userData (safeStorage-wrapped when available, mode 0o600
otherwise). Wire format: enc:v1:<b64-iv>:<b64-ct+tag>.
Idempotent (does not double-encrypt), and transparently passes
legacy plaintext through decryptField() so the migration is
forward-compatible.
electron/ipc/handlers/entities.cjs
applyEncryptionToWrite() encrypts on insert/update; the
__SET__ sentinel preserves an existing credential when the
renderer round-trips a redacted form.
redactSecretsForRenderer() ensures the cleartext never leaves
the main process.
electron/functions/index.cjs
pushToEHR() now decrypts via decryptField() before adding the
Authorization header. Corrupt ciphertext fails closed with a
clear "re-enter the API key" message.
electron/database/migrations.cjs (v10)
encrypt_legacy_ehr_api_keys: re-encrypts every existing plaintext
row in place. If encryption is unavailable (headless test envs),
nulls the column rather than leaving plaintext.
tests/secretEncryption.test.cjs
10 tests: round-trips, IV randomness, idempotency, legacy
pass-through, tampering detection, label/key isolation, cache
persistence.
================================================================
C-8: OIDC desktop SSO with PKCE + system browser
================================================================
electron/auth/oidcDesktop.cjs
PKCE S256 flow (no plain, no implicit), random state + nonce,
constant-time state comparison, https-only endpoint validation,
https-only token exchange, configurable 5-minute pending-flow
TTL, single-flight (concurrent starts cancel prior pending).
Decodes id_token claims; JWKS signature verification is a
documented follow-up (PKCE binding already gates replay).
electron/main.cjs
Registers transtrack:// custom protocol. Adds single-instance
lock + open-url (macOS) + second-instance (Win/Linux) handlers
that route transtrack://auth/callback?... to the OIDC module
and then to the SSO session finalizer.
electron/ipc/handlers/ssoCallback.cjs
Final stage: looks up the local user by lowercased email AND
sso_enabled=1; refuses if not provisioned. Mints a TransTrack
session row, updates last_login, records the OIDC subject for
audit correlation. Never exposed as a renderer IPC channel.
electron/ipc/handlers/auth.cjs
auth:ssoStart / auth:ssoCancel IPC channels. shell.openExternal
pushes the IdP authorization URL to the system browser.
electron/database/migrations.cjs (v11)
add_sso_columns_and_app_settings: sso_enabled + sso_subject on
users, generic app_settings k/v table for OIDC issuer + client
ID configuration.
electron/preload.cjs
Bridges window.electronAPI.sso.{start,cancel,onCompleted}.
src/pages/Login.jsx
"Sign in with your organization (SSO)" button alongside the
existing email/password form. Subscribes to the auth:ssoCompleted
broadcast and triggers AuthContext.refreshAuth() on success.
src/lib/AuthContext.jsx
Exposes refreshAuth (= checkAppState) so post-callback components
can re-query the session without coupling to internal state.
src/api/localClient.js
api.sso.* + browser-dev mock.
docs/SSO_DESKTOP.md
Operator guide (Azure AD / Okta / Auth0 / Google setup), threat
model, what this is NOT (no SCIM, no group mapping, no IdP
sign-out propagation).
tests/oidcDesktop.test.cjs
7 tests: PKCE generation, JWT decode, https-only enforcement,
startFlow argument validation, callback-without-pending rejection,
cancelFlow lifecycle.
================================================================
C-9: Stop leaking the bootstrap admin token to stdout
================================================================
electron/database/init.cjs
The first-launch banner used to echo the password file PATH and
earlier revisions echoed the password itself. stdout is captured
by RMM tooling, journald, PowerShell transcripts, Windows Event
Forwarding, and Electron's own log files. The banner now states
that the token is in the file and ONLY prints the path.
electron/ipc/handlers/auth.cjs
auth:changePassword now calls purgeSetupTokenFile() the first
time a user rotates out of must_change_password=1. The token
file is overwritten with zeros before unlink to defeat naive
undelete. Best-effort; never throws from this path.
================================================================
Test commands
================================================================
npm run test:license # 30 tests (encryption + licensing)
npm run test:sso # 7 tests (OIDC desktop)
================================================================
Important deployment note (READ BEFORE FIRST RELEASE)
================================================================
electron/license/publisherPublicKey.cjs ships with a DEVELOPMENT
Ed25519 public key generated by `npm run license:keypair`. Before
cutting a v1.x.0 release that will be sold:
1. Generate a production keypair on an offline workstation:
npm run license:keypair -- --force
2. Paste the printed PUBLIC_KEY_BASE64 into publisherPublicKey.cjs
3. Bump LICENSE_PROTOCOL_VERSION from 1 to 2
4. Move keys/license/license-private.pem to your offline vault
and add the same value to the server as a Docker secret at
the path referenced by LICENSE_PRIVATE_KEY_PATH (for the
Stripe webhook to be able to sign licenses).
The License page surfaces an amber "Development build" warning
whenever the dev key is in use, so this cannot silently slip into
a customer build.
Co-authored-by: Cursor <cursoragent@cursor.com>
…vendors
CRITICAL_ACTIONS_REQUIRED.md documents the 4 production-readiness
audit items that genuinely cannot be closed inside the codebase
because they require contracts, payments, or third-party signatures:
C-2 Legal entity formation + vendor domain + business email
C-4 Independent penetration test
C-5 Executed IQ / OQ / PQ validation package
C-11 E&O + cyber liability insurance
Each item has:
- a concrete vendor list with indicative pricing
- what underwriters / hospital procurement actually ask for
- a copy-pasteable initial outreach email
- a final "buyer can flip through this and check every box"
checklist
This file lives at repo root rather than under docs/ because it is
the operational TODO for the founder, not API documentation. It
should be removed (or moved under docs/internal/) before the repo
is shared with a buyer's diligence team.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two distinct CI failures introduced by the C-1/C-8 work:
1. build: tests/components/Login.test.jsx (5 failures)
- The renderer's api.sso.onCompleted wrapper in src/api/localClient.js
dereferenced window.electronAPI.sso.onCompleted unconditionally. The
vitest setup only stubs auth/functions/entities namespaces, so any
test that renders <Login /> blew up with
"Cannot read properties of undefined (reading 'onCompleted')".
Wrapped all api.sso.* and api.license.* paths in optional chaining
and made onCompleted return a no-op unsubscribe when SSO isn't
wired up, so React effect cleanup is always safe.
- The new "Sign in with your organization (SSO)" button collided
with the test's /sign in/i regex. Anchored the queries to
/^sign in$/i so they uniquely select the submit button.
2. CodeQL: 4 js/file-system-race (TOCTOU) warnings
Removed the existsSync()-then-act pattern from the four newly-added
files CodeQL flagged. Each now uses one of:
- try { readFileSync } catch ENOENT (storage, machineId)
- openSync('r+') / closeSync (purgeSetupTokenFile)
- writeFileSync({ flag: 'wx' }) (license-keypair.mjs)
These eliminate the time-of-check / time-of-use window while keeping
the existing behavioural contracts. All 20 license tests, 10 secret
encryption tests, 7 OIDC desktop tests, and the full 119-test vitest
suite still pass locally.
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes 8 of the 12 critical production-readiness audit items in code, and ships the operational checklist for the other 4 that require vendors / contracts / signatures. Split into 5 reviewable commits.
cc8199b.tsfiles (~3,000 LOC) from top-levelfunctions/d3a71c9--for-saleflag promotes code-signing gates from optional to mandatory; newrelease.ymlworkflow enforces it onv*.*.*tags43facfcissued_licensestable; full operator guide6c182a6transtrack://protocol handler; secure-overwrite of the bootstrap admin token file on first password change1befb16CRITICAL_ACTIONS_REQUIRED.md— vendor list + indicative pricing + outreach email templates for C-2 (legal entity), C-4 (pen-test), C-5 (IQ/OQ/PQ), C-11 (insurance)Net effect
New tests
37 new tests, all passing locally:
npm run test:license→ 30 tests (encryption + Ed25519 sign/verify + expiry + machine binding + activation + trial state)npm run test:sso→ 7 tests (PKCE, JWT decode, https-only enforcement, flow lifecycle)Important deployment caveat
electron/license/publisherPublicKey.cjsships with a development Ed25519 public key generated bynpm run license:keypair. Before cutting a paid release, the operator must:npm run license:keypair -- --forceon an offline workstationPUBLIC_KEY_BASE64into that fileLICENSE_PROTOCOL_VERSIONfrom1→2keys/license/license-private.pemto the offline vault + the server's Docker secret pathThe License UI surfaces an amber "Development build" warning while the dev key is in use, so this cannot silently slip into a customer build.
Test plan
npm run test:license(30/30 pass)npm run test:sso(7/7 pass)npm run release:checkpasses with signing gates as optionalnpm run release:check:for-salecorrectly fails without signing credsnpm run license:issue→ activation succeeds; UI flips toActiveenc:v1:...ciphertext, not the plaintextCRITICAL_ACTIONS_REQUIRED.mdis acceptable in repo root, or move underdocs/internal/Files