Skip to content

Inventivus V1.9.1

Latest

Choose a tag to compare

@Therealsboy8 Therealsboy8 released this 07 Sep 18:01
· 2 commits to main since this release
8fe0fd3

Inventivus

Engineering mission control — a local-first Windows desktop application for
engineering calculators, machine architectures, native app overlays, version
control, and a built-in AI assistant named Athena.

Inventivus is an Electron app. A React + TypeScript SPA runs in the renderer, a
Fastify + SQLite backend runs as a child process of the Electron main process, and
everything — projects, simulation history, git repos, encrypted credentials — stays
on the user's machine. There is no hosted application server.


Changelog

v1.9.1 — 6 September 2026

Security and production-readiness release. Focus: making a payment
unconditionally provable, tightening the shipped attack surface, and keeping UI
state consistent across the whole app.

Critical: a payment is now provable even if activation never completed

v1.8.8 bound the buyer's GitHub account to their Stripe customer — but only when
/licence/claim succeeded, and that call runs on the customer's machine after a
inventivus:// deep link. Every way that hand-off could fail (app closed before
the browser redirected, protocol handler not registered, network dropped, or the
earlier activation bug) left a paying customer with no binding at all, whose
"Sign in with GitHub to restore" correctly reported no subscription found. The
only fix was hand-writing KV keys. Three independent changes close that gap:

  • The GitHub identity is now written to permanent Stripe objects. Checkout
    stamps it onto the subscription and customer (and client_reference_id),
    not just the short-lived Checkout Session. Stripe itself now holds the binding.
  • Provisioning happens on payment, not only on claim. The issuer acts on the
    checkout.session.completed webhook — server-to-server, so no customer-side
    failure can skip it. Idempotent with the claim path; whichever lands first
    wins. An unpaid session provisions nothing, and an existing Enterprise record
    is never downgraded.
  • Restore falls back to Stripe when the local link is missing. If no KV link
    exists, the issuer searches Stripe by GitHub identity and rebuilds the record.
    Only active/trialing subscriptions qualify, so a cancelled plan is never
    resurrected. Payment alone is now sufficient proof of purchase.

Security hardening

  • The shipped renderer no longer permits eval() or inline script. The CSP
    was one literal <meta> tag carrying 'unsafe-inline' 'unsafe-eval' in
    script-src — required by Vite's dev server, and therefore shipped to every
    install. Since Athena renders model output into that window, this was a live
    concern. The policy is now injected per environment: dev keeps what HMR needs,
    the packaged build ships script-src 'self'. Same fix for the HUD overlay,
    which floats over other applications and carries the local API token.
    object-src, frame-src, base-uri and form-action are pinned closed.
  • The licence issuer is rate-limited. It is a public origin holding the
    Stripe secret key and had no metering on any endpoint. Seat-granting
    (/team/join), selling (/checkout) and every Stripe-amplifying path
    (/licence/claim, /licence/refresh, the GitHub poll) are now bounded
    per client address, with Retry-After. Stripe's own webhook is deliberately
    exempt (it retries on non-2xx), and a KV failure fails open so a broken
    limiter cannot take checkout down with it.

Consistency: state is shared across the app

  • An open workspace stays open. It lived in useState on the project page,
    so navigating to another tab and back showed "Select a Workspace" and looked
    like the work had been closed. Active workspace and active project are now
    persisted server-side in SQLite, so they survive navigation, a reload, and the
    app being closed and reopened. Deliberately an allowlisted set of keys, not a
    general key/value store.

Fixes

  • Local-model crashes explain themselves. An Ollama GPU/CUDA failure used to
    surface as a raw HTTP 500 dump quoting Windows' stack-buffer-overrun text
    ("could potentially allow a malicious user to gain control") — alarming,
    misleading, and unactionable. Provider failures are now mapped to plain
    language with a concrete next step (update drivers, restart Ollama, run a
    smaller model, start Ollama, pull the model, check the API key), with the
    technical detail kept separate. Applies to every AI path, not just chat.
  • One version number. /api/health and the About panel each hard-coded
    0.1.0, so a release bumped the installer and left the app reporting the old
    version. Both now derive from package.json.
  • yarn verify:editions additionally asserts the packaged version, the baked-in
    issuer URL and public key, and the strict CSP in both editions.

v1.8.8 — 6 September 2026

Critical: subscriptions now survive reinstalls, updates, and new machines

Previously a customer's Pro/Enterprise status lived only in a local licence
token. A reinstall, an app update that reset app-data, or moving to a new machine
wiped that token — and there was no way to prove ownership again, so paying
customers silently dropped to Free with no recourse. Fixed end to end:

  • Durable proof-of-purchase via GitHub. Every purchase now binds the buyer's
    GitHub account to their Stripe customer in the issuer (captured at checkout).
    A new "Restore purchase — sign in with GitHub" control on the Upgrade page
    re-grants the subscription on any machine, from the customer's GitHub account
    alone — no local state required. One click, and the install is linked so future
    updates/reinstalls self-restore.
  • Recovery for customers who paid earlier. Signing in with GitHub while a
    licence is installed links that purchase to the account, so pre-existing
    customers (including the first testers) become restorable too.
  • Cancellation alerts + faster propagation. The issuer now exposes a
    signature-verified Stripe webhook (/webhooks/stripe). It updates the stored
    subscription status the moment Stripe reports a change and posts a merchant
    alert to a configured Slack/Discord webhook on cancellation — Stripe's own
    emails only notify the customer, not the merchant. Entitlement is still decided
    by live Stripe polling, so the webhook is never a single point of failure.
  • Setup steps: issuer/SUBSCRIPTION-PERSISTENCE.md.
  • Added 9 issuer tests (solo restore, cancelled-subscription refusal, link-at-
    token, GitHub rename, webhook signature + notify); 1725 tests total.

v1.8.7 — 6 September 2026

Critical: payment activation and subscription management

Payments now complete on customer machines. Previously a customer could pay
through Stripe Checkout and never receive their features, because the app was
never told the payment succeeded — and with no recorded licence, the "Manage
subscription" button never appeared, leaving customers unable to cancel. Root
cause and fix:

  • Checkout return had no path back into the app. Stripe Checkout opens in the
    customer's browser, so the app cannot see the redirect that carries the
    session_id needed to claim the licence. The issuer now serves a /return
    landing page that hands the session id back to the app through the
    inventivus://billing deep link (with an on-screen activation code and copy
    button as a manual fallback). The app claims the licence automatically on
    receipt, installs the signed token, and switches to the paid tier.
  • inventivus:// links launched a second app instance instead of reaching the
    running one.
    Added the Electron single-instance lock, without which the
    second-instance handler never fires on Windows — so a customer who had the
    app open when they paid would spawn a colliding second process and lose the
    deep link entirely. This was the difference between "payment activates" and
    "payment silently lost" for the common case.
  • Subscription management/cancellation now works. "Manage subscription"
    appears once a real licence is installed and opens the Stripe Customer Portal
    via the issuer (authenticated by the licence token, so no local Stripe key is
    needed). The portal's return link deep-links inventivus://billing?refresh=1,
    so a cancellation or plan change is reflected in the app within seconds rather
    than on the next hourly refresh.
  • Added issuer tests for the /return, /portal-return, and /cancelled
    landing pages and for the checkout success_url wiring (1716 tests total).

v1.8.6 — 6 September 2026

Bug fixes

Workspace creation

  • Fixed EasyEDA Pro .eprj (and .eprj2, .elib) files being rejected at workspace creation with "file type not supported". The extension whitelist in server/utils/path-safety.ts had drifted out of sync with the app catalog and was missing every EasyEDA format plus ~40 other catalog extensions (.kicad_pro, .fcstd, .prjpcb, .sldprt, and more). Added a regression test that enumerates the full catalog so this class of bug cannot silently reappear.

Onboarding

  • The AI setup step is now skippable. Users without Ollama or an API key can click "Skip for now" and configure AI later in Settings → AI Provider. The app opens normally in the meantime; document analysis returns a clear "no AI configured" message rather than failing silently.
  • Added a collapsible step-by-step setup guide per provider (install commands, model pull commands, exact URLs, port numbers) shown inline on the AI step.
  • Rewrote the "connection failed" advice into numbered, actionable troubleshooting steps for each provider.

Team / sharing

  • Free-tier project-share cap could be bypassed by using the bulk share path (POST /api/team/share/apply with no project_id). Fixed: bulk path now pre-counts and enforces the cap.
  • Task delete (DELETE /api/team/tasks/:id) had no authorisation — any team member could delete any task. Fixed: only the task creator or a team owner may delete.
  • POST /api/team/events?month= had no format validation; an unvalidated value was used as a SQL LIKE operand, returning all events. Fixed: reject anything not matching YYYY-MM.
  • Optional fields on events and tasks (description, time, assigned_to_member_id) were stored as empty strings instead of NULL, breaking IS NULL queries. Fixed to store NULL.
  • Task unassign was impossible — assigned_to_member_id only accepted a string, not null. Fixed: accepts z.string().nullable().
  • Post display-name fallback used the team name instead of member_id. Fixed.

Payments / Stripe

  • customer.subscription.deleted webhook cleared tier and status but left subscription_id set. Subsequent subscription refresh and portal calls would then attempt to operate on the deleted subscription, returning 402/404 from Stripe. Fixed: null out subscription_id on deletion.
  • Success page copy said "Pro subscription is active" for all plans, including Enterprise customers. Fixed to neutral wording.

Version control

  • Data loss: renaming a workspace then running a scoped commit silently dropped the renamed workspace's manifest entry (matched by name instead of stable repo_path), causing pruneOrphans to delete the file from the repository. Fixed: carry-forward matches by repo_path. Added a regression test.
  • POST /vcs/unstage returned a raw 500 stack trace when called on a file that was never committed to HEAD. Fixed: returns 400 with the git error message.
  • Commit error detection in VersionControl.tsx matched (e as Error).message.includes('409') — any error message containing "409" would show "Nothing to commit" instead of the real error. Fixed to e instanceof ApiError && e.status === 409.
  • stageAll() returned the count of all files with any status rather than files newly staged. Fixed.

Cross-domain checking (core feature)

  • disciplineOf returned 'other' for EDA, firmware, and CAD nodes not yet placed in a container, in three separate locations: drift.ts, consequence.ts, and inference.ts. This silently excluded real cross-discipline connections from drift reports, consequence assessments, and the inference brief sent to the AI model — the most critical output of the system. Fixed in all three with a detector-based fallback.
  • EasyEDA Pro document detector ID was hardcoded to 'easyeda-schematic' instead of 'document-projection', breaking the provenance chain for .eprj files in drift detection. Fixed.
  • Support-part warnings were emitted twice in EasyEDA Pro document detection (once in warnings, once via omitted). Fixed.
  • Detection warnings were concatenated twice into the machine-links response, doubling every warning shown to the user. Fixed.
  • partStates was not passed to computeStaleness in the /inference endpoint (unlike /staleness), so CAD parts from container documents were always shown as current to the AI model regardless of actual state. Fixed.
  • Inference test fixture DRIFT was missing required revised and consequences fields, causing tests to exercise the wrong struct shape. Fixed.

Production release — v1.9.1

The shipping build. Two Windows installers come out of one codebase:

Artifact Path Who it is for
Inventivus-Setup-1.9.1.exe inventivus/release/standard/ Customers. Free tier with paywall; Pro/Enterprise unlocked by a signed licence.
Inventivus-Unlocked-Setup-1.9.1.exe inventivus/release/unlocked/ Internal. Same code with resolveEntitlement short-circuiting to Enterprise.

Both install side by side and share one data directory, so switching editions
never migrates or orphans a database.

Release gate

cd inventivus
yarn typecheck        # 5 tsconfig projects
yarn test             # 1828 tests / 76 files
yarn package:both     # build once, package twice
yarn verify:editions  # 82 assertions against the packaged .asar

verify:editions reads values back out of the shipped archive rather than
trusting the source tree, and it fails the release on a regression. Alongside the
edition and EULA checks it asserts every security fix listed below is present in
the artifact a customer actually runs.

Matching gotcha for anyone extending that script: tsc rewrites module
references, so app.isPackaged ships as electron_1.app.isPackaged and
validateExecutable(x) as (0, launch_safety_1.validateExecutable)(x). Anchor
assertions on the argument or a distinctive string literal, never the callee
shape — a source-shaped pattern reports a false FAIL on a correct build, and the
inverse mistake would silently pass a broken paywall.

Known limitations in this build

  • Unsigned. No code-signing certificate is configured, so Windows SmartScreen
    warns on first run (More info → Run anyway). Verify with
    Get-AuthenticodeSignature — it reports NotSigned. Signing needs an EV or OV
    certificate; nothing in the code has to change.
  • Real-time collaboration is not deployed. partykit.json still carries the
    placeholder host and partykit.inventivus.app does not resolve. This fails
    safe: sharing, joining, cloning and syncing all work over GitHub, and
    collaborators simply do not see each other's cursors. Deploying it is
    npx partykit login && npx partykit deploy, then setting the real host.

Security model

The app takes live payments and opens files a collaborator can influence, so the
boundaries are worth stating explicitly.

What the app cannot do

  • It cannot mint a licence. Only the public key is embedded. Signing happens
    in the Cloudflare Worker, which is the sole holder of the private key and the
    Stripe secret. No customer install ever contains sk_live_… — asserted by
    verify:editions.

  • It cannot be unlocked from the environment. Three switches decide
    entitlement, and in a packaged build every one of them ignores the launching
    shell:

    Variable Effect if trusted Gate
    INVENTIVUS_EDITION unlocked → all features read from resources/edition.json when packaged
    NODE_ENV=development INVENTIVUS_DEV=1 → Enterprise, no licence !app.isPackaged && …
    INVENTIVUS_LICENCE_PUBLIC_KEY verifies self-signed licences baked key wins when packaged

    The compiled backend is a plain Node process that can be run directly, so it
    carries a second, independent gate: isDev() also requires
    INVENTIVUS_PACKAGED !== '1', which the main process sets from
    app.isPackaged.

Enforcement is server-side

Gated routes answer 402 regardless of what the renderer draws, and quantity
caps are checked on every create path — including POST /api/projects/join,
not just POST /api/projects. Returning a features map to the renderer is not
enforcement: /api/hud withholds the Pro-gated structure and problems
payloads rather than relying on the HUD to hide them.

Caps apply on creation only, so a user who drops from Pro keeps everything
they already made.

Team seats

Seat entitlement is authoritative on the issuer, which re-reads the roster and
the live Stripe subscription on every licence issue — a cancelled plan cannot be
kept alive by anything cached locally. The local SQLite team tables are a mirror
that grants nothing.

Seat holders receive enterprise tokens, so a tier check is not a sufficient
authorisation boundary: canAdministerTeam() additionally requires
team.role === 'owner', otherwise any member could mint unlimited further seats
out of one subscription.

Renderer and IPC

  • Strict CSP in the packaged build — script-src 'self', no 'unsafe-inline',
    no 'unsafe-eval', injected per environment by a Vite plugin so the dev
    server's relaxed policy cannot ship. Athena renders model output into that
    window, which is why this matters.
  • contextBridge only; no nodeIntegration.
  • Filesystem IPC runs through PathGuard, which denies credential stores inside
    the home tree — including browser profiles (Chrome, Edge, Brave, Chromium,
    Firefox, Thunderbird), where saved passwords and session cookies live.
  • Both spawn sites validate the executable, so a .bat/.ps1/.hta or UNC
    connection path cannot turn "open a document" into running an interpreter.
  • inventivus:// deep links are host-allowlisted, not merely shape-checked;
    a link from any web page cannot point live sync at a foreign server.
  • Data pulled from a shared repository is confined on the destination, not
    only the source. A crafted .inventivus/workspaces.json cannot write outside
    its own folder — the previous behaviour allowed a write into the Windows
    Startup folder on the victim's next git pull.

Issuer worker

  • Licence tokens are signature-verified before their subject is used. A
    Stripe customer id alone is not a credential — it is a username, and it appears
    in URLs and support tickets.
  • Expiry is deliberately not checked on /licence/refresh: renewing an expired
    token is what that endpoint is for. Freshness comes from KV plus a live Stripe
    check.
  • Stripe webhooks are HMAC-verified with a constant-time compare and a 300-second
    replay window. The webhook is exempt from rate limiting (Stripe retries on
    non-2xx); everything else is metered per IP on unspoofable CF-Connecting-IP,
    failing open so a KV outage cannot stop customers refreshing.
  • GET /notify/status reports whether merchant alerts are landing, and
    POST /notify/test sends one and returns the true HTTP status. These exist
    because the alert path is silent by design and a mismatched webhook secret is
    otherwise unobservable — Stripe's pending_webhooks: 0 reads the same whether
    an endpoint accepted the event or no endpoint was subscribed at all.

Repository layout

.
├── inventivus/          ← the actual product. Everything below this line is here.
├── memory/PRD.md        Original product requirements and backlog
├── backend/             Emergent scaffold (FastAPI "Hello World" + Mongo) — unused
├── frontend/            Emergent scaffold (CRA + shadcn/ui starter)      — unused
├── tests/               Empty pytest package left by the scaffold
└── test_result.md       Scaffold testing-protocol file

backend/, frontend/, and tests/ are leftovers from the project generator. They
are not wired into the desktop app and can be ignored (or deleted) — the Fastify
backend the app actually runs is inventivus/server/.


Quick start

Requires Node 20+ and Yarn 1.x.

cd inventivus
yarn install
yarn dev

yarn dev builds the server, then runs the Vite dev server on :5273, a
TypeScript build of the Electron main + preload into dist-electron/, and
Electron itself — which spawns the Fastify backend on a free port.

yarn dev rebuilds the server first for a reason: without it, a change to
server/ has no effect on the running app because Electron spawns the last
compiled dist-server/ bundle. If a backend fix seems to do nothing, compare
stat server/<file>.ts against stat dist-server/<file>.js.

First launch opens the onboarding wizard. To skip the paywall while developing:

yarn dev:unlocked

Full setup walkthrough, including where each Stripe key goes:
inventivus/SETUP.md.
Deeper reference on architecture and internals:
inventivus/README.md.


Stack

Layer Tech
Shell Electron 43 (contextBridge IPC, no nodeIntegration)
Renderer React 18 · TypeScript · Vite 6 · Tailwind CSS
State Zustand (global) · TanStack Query (server)
Graph / canvas React Flow
Backend Fastify 5, spawned as a child process on launch
Storage SQLite via better-sqlite3 (WAL + FTS5 full-text search)
Version control isomorphic-git — real git repos per project
Collaboration Yjs + PartyKit (y-partykit)
Native interop koffi FFI for Win32 window tracking
AI Ollama · LM Studio · OpenAI-compatible · Anthropic
Payments Stripe Node SDK + Ed25519-signed licence tokens
Packaging electron-builder → Windows NSIS installer

What's in the app

  • Mission — dashboard and telemetry across projects.
  • Calculators — truss FEM, beam, buckling, and co-simulation, with sandboxed
    controller expressions and live charting.
  • Machines — visual architecture layout wired to simulation runs.
  • Integrations — register any executable (CAD tool, simulator, terminal),
    launch it, and park a frameless HUD window in front of it. The HUD is a genuine
    second window owned by the launched app, not a reparented overlay — see
    SETUP.md § "The HUD is not an overlay".
  • Version control — git plus entity-level history, mirrored to a workspace.
  • Athena — full-page chat and a floating dock, SSE-streamed from the local
    backend so API keys never reach the renderer.
  • Projects — CRUD, invites, join flow, status control, GitHub sign-in via
    device flow.
  • Global search — SQLite FTS5 behind a Ctrl/+K
    command palette.

Editions and the free/Pro split

Two builds ship from one codebase with a single branch taken:

  • Standard — the paywalled build customers install.
  • Unlocked — same code, resolveEntitlement short-circuits to Enterprise.
    Enterprise rather than Pro so "every feature on" keeps meaning that as tiers
    grow.

What each tier gets

Free Pro Enterprise
Projects 5 25 unlimited
Integrations 3 unlimited unlimited
Calculators, Machines (simulations, workflows)
HUD live help, project structure, focus tools
Version control, Athena, global search
Joining a team
Creating and running a team ✓ (up to 35 seats)

Version control, collaboration and Athena are free on purpose: a lapsed
subscription must never appear to have eaten someone's work. Limits are enforced
on creation only, so a user who drops from Pro keeps everything they have.

Joining a team is free by design — a member has to be able to accept the seat
somebody already paid for, which cannot happen if joining itself sits behind the
tier the seat grants. Their licence is then issued as Enterprise for as long as
the team's subscription is live.

Pro is proved by an Ed25519-signed licence token minted by the issuer worker
— never by a local database column. Writing tier = 'enterprise' into SQLite by
hand grants nothing, because resolveEntitlement reads only the token. See
Security model for the full boundary.


Building the installers

cd inventivus
yarn package:standard   # → release/standard/Inventivus-Setup-1.9.1.exe
yarn package:unlocked   # → release/unlocked/Inventivus-Unlocked-Setup-1.9.1.exe
yarn package:both       # build once, package twice
yarn package:dir        # unpacked build, faster for testing

The installer filename tracks version in inventivus/package.json, which is
also the single source for /api/health and the Settings About panel.

electron-builder rewrites package.json in place to apply per-edition
metadata, and does not always restore it — leaving a tracked file with
scripts and devDependencies stripped, so the next yarn typecheck fails
with "Command not found". Recover with git checkout -- package.json and
re-apply the version bump. verify:editions asserts the rewrite did not
survive, so run it after packaging.


Scripts reference

yarn dev                # Vite (:5273) + Electron + live backend
yarn dev:unlocked       # same, with Enterprise entitlements
yarn build              # renderer + electron main + server
yarn typecheck          # tsc --noEmit across all 5 projects
yarn lint               # typecheck && eslint
yarn test               # vitest run  (1828 tests)
yarn test:watch         # vitest, watching
yarn start:server       # backend only, for debugging

yarn verify:editions    # 82 assertions against the packaged .asar
yarn verify:persistence # assert userData survives reinstall
yarn verify:webhooks    # sign synthetic Stripe events → SQLite
yarn verify:pathb       # polling-based subscription refresh
yarn verify:ai          # AI provider proxy round-trip

yarn issuer:dev         # local Cloudflare Worker on :8788
yarn issuer:keygen      # generate an Ed25519 licence keypair
yarn issuer:secrets     # push secrets to Cloudflare
yarn issuer:deploy      # wrangler deploy
yarn issuer:tail        # live worker logs
yarn issuer:test        # vitest run issuer  (81 tests)

Unit tests are colocated as *.test.ts next to their subjects and run with Vitest
(vitest.config.ts), covering electron/, server/, src/, issuer/ and
partykit/.


Where data lives

Path Contents
<userData>/inventivus.db SQLite database (WAL + FTS5)
<userData>/config.enc Stripe + AI + GitHub credentials, AES-256-GCM
<userData>/.inventivus.key 32-byte key generated on first launch
<userData>/repos/project-<id>/ per-project git repositories

<userData> is %APPDATA%/inventivus/ on Windows — lowercase, and identical
for both editions
, because app.setName(DATA_IDENTITY) pins it. Changing that
string is a silent data migration, which is why verify:editions asserts it.

The key file is written 0600, which is close to meaningless on Windows: treat
<userData> as readable by anything running as that user. The encryption
protects the credentials at rest against copying the file elsewhere, not against
the local user.


Further reading

Document Covers
inventivus/SETUP.md First-time setup, keys, HUD, licensing, pitfalls
inventivus/README.md Architecture and module-level reference
inventivus/issuer/README.md Deploying the licence issuer (~15 min)
INFRASTRUCTURE.md Live infrastructure: issuer, Stripe wiring, secrets
VERIFICATION_CHECKLIST.md Manual pre-release checklist
memory/PRD.md Original requirements and backlog
FILE_OPERATIONS_FIX.md File open/save handling notes

Support

Licensing or billing problems — including a payment that did not activate — go to
inventivustech@gmail.com. A customer whose subscription is not recognised
should sign in with GitHub from the banner at the top of the app, or from
Upgrade → Restore purchase; that reconciles against Stripe directly and does
not depend on the original activation having completed.


License

Proprietary. © Inventivus. All rights reserved.

The end-user licence agreement shown on first launch is the
Inventivus Single-User License, bundled into the renderer and asserted by
verify:editions.