Skip to content

feat(nexus): remove personal API keys, sign in with OAuth only - #7

Open
MotherSphere wants to merge 5 commits into
mainfrom
feat/nexus-oauth-only
Open

feat(nexus): remove personal API keys, sign in with OAuth only#7
MotherSphere wants to merge 5 commits into
mainfrom
feat/nexus-oauth-only

Conversation

@MotherSphere

Copy link
Copy Markdown
Member

For Nexus review. Not to be merged until a client_id is issued - see below.

Nexus's API team requires all personal-API-key usage removed from the client before issuing a client_id, including as a fallback, and asked to see a build without it.

What was removed

  • Credential is a single variant. There is no APIKEY header path.
  • choose_credential is Bearer / refresh / nothing. A lapsed session that cannot be renewed now reports itself instead of falling back to a key.
  • NexusCreds has no api_key field; nexus.ini no longer writes one.
  • The eidos nexus key CLI command is gone.
  • The Settings dialog has no field to type a key into.
  • The token endpoint may still return api_key beside the tokens. It is no longer parsed - a key kept "just in case" is exactly what the requirement rules out.

An api_key= line left in a user's nexus.ini by an older version is passed through untouched rather than rewritten away. Not reading it is the requirement; deleting someone's file is not.

What was added

The OAuth flow existed as a library and had never been wired to a user action, because there was no client_id to test it with. Removing the key without wiring it would have produced a build with no way to authenticate at all, which demonstrates nothing.

Settings -> Nexus now runs the whole flow: PKCE S256 challenge, browser hand-off, loopback listener on 127.0.0.1 only, code exchange, session stored in nexus.ini. Sign out sits beside it.

The consequence, stated plainly

Eidos ships no default client_id and will not fall back to identifying itself as another application. With personal keys gone and no client_id issued, this build has no Nexus access at all. That is the intended state rather than an oversight, and the error messages say so.

The moment a client_id is issued, this becomes the shipped version.

615 tests green, clippy clean under CI's exact command.

Nexus's API team will not issue a client_id while the client can use a personal
API key, even as a fallback: those keys are documented on their side as being for
testing and personal use, not for a distributed application. They asked to see a
build without any such usage.

So it is gone, not disabled. `Credential` is one variant. `choose_credential` is
Bearer, or refresh, or nothing - a lapsed session with no renewable token now
reports itself instead of reaching for a key. `NexusCreds` has no `api_key`
field, `nexus.ini` no longer writes one, the `eidos nexus key` command is gone,
and the Settings dialog has no field to type one into. The token endpoint may
still return `api_key` alongside the tokens; it is no longer parsed, because a
key kept "just in case" is exactly what the requirement rules out.

The OAuth flow existed as a library and had never been wired to a user action -
there was no client_id to test it with, so nothing called it. Removing the key
without wiring it would have produced a build with no way to authenticate at all,
which demonstrates nothing. Settings -> Nexus now runs the whole dance: PKCE S256
challenge, browser hand-off, loopback listener on 127.0.0.1, code exchange, session
stored. Sign out is beside it.

An `api_key=` line left in someone's nexus.ini by an older version is passed
through untouched rather than rewritten away - not reading it is the requirement,
deleting a user's file is not.

Eidos therefore has no Nexus access at all until Nexus issues a client_id. That is
the intended state, and the error messages say so rather than failing vaguely.

615 tests green, clippy clean.
The request path no longer sends one; the comment beside it still said it did.
MotherSphere added a commit that referenced this pull request Aug 4, 2026
main.rs was 2493 lines - every subcommand, the whole launch pipeline and the
profile staging in one file. Pure moves, routed by item name:

  main.rs      274   dispatch, usage, and the Nexus commands
  launch.rs    481   eidos play: extender swap, mount, run, capture
  prepare.rs   470   staging the profile into the prefix, and reading it back
  tools.rs     281   eidos tool
  tests.rs     275   the unit tests, file-backed
  prereqs.rs   218   eidos prereqs
  nxm.rs       157   the nxm:// handler
  sort.rs      148   eidos sort (LOOT)
  install.rs   134   eidos install / import
  export.rs    123   eidos export
  games.rs      50   eidos games / init

The Nexus trio (cmd_nexus, nexus_client, nexus_key_path) deliberately stays in
main.rs: PR #7 rewrites exactly those functions, and moving them now would turn
a pending review branch into a wall-to-wall conflict. They move once #7 lands.

Verified as the fuse split was: item inventory identical (the nine additions are
the test module's own fns and its Tmp helper surfacing to file level), workspace
suite green, and the split binary smoke-run against the real machine.
MotherSphere added a commit that referenced this pull request Aug 4, 2026
…od (#12)

profile.rs was one 800-line impl Profile plus its satellites. The unit of the
move is the METHOD here - Rust allows inherent impls across files, so each
concern gets its own impl Profile block beside the free items it owns:

  profile.rs    91   Profile and SaveEntry themselves, the path roots
  modlist.rs   385   modlist.txt round-trip, ListTrust, lifecycle
  plugins.rs   350   plugins.txt, the locked order, snapshot + loss guard
  inis.rs      327   seeding, deploy, capture, the reversible tweaks
  saves.rs     236   enumeration, co-saves, crash-artifact detection
  tests.rs     889   the unit tests, file-backed

settings.rs deliberately untouched: PR #7 and PR #8 both edit it, and a move
now would conflict with both.

Verified at the level the cut happened: 89 functions before, 89 after, none
lost and none invented. Workspace suite green, clippy clean.
Brings in the six PRs merged since this branch was cut (#8 parity, #9-#12
crate splits, #13 the overwrite index) plus the 1.3.0 release.

Two conflicts, both in the Settings screen, which main rewrote into Colony's
collapsible sections while this branch was replacing the API-key field with
OAuth sign-in. Resolved by keeping main's structure and putting the sign-in
UI inside it: the Account section now holds the sign-in / sign-out pair and
the session status instead of a masked key field, and the app state keeps
`settings_expanded` while dropping `settings_api_key`, which no longer has
anything to hold.
…the budget

Two Terms-of-Service points Nexus raised reviewing the OAuth-only build.

**Age restriction.** The gate lives inside the client, in `RemoteMod::from_payload`,
not at the display sites. Three reasons it cannot sit further out: the descriptive
text reaches a dozen call sites across the CLI and the GUI, so gating each one is
a leak per site added later; the text is PERSISTED before anything is displayed
(`write_download_meta` puts `modName` in the sidecar, and `eidos-install` turns
that into the directory name under mods/), which no display-side check could take
back; and put here, the compiler can enforce it. A mod the account may not see has
its name, summary and category blanked before the struct exists, and the `ModGate`
minted alongside them is what `file_info` and `download_link` demand - so there is
no way to reach a file or a download link without having passed the lookup that
resolves the rating. `nxm.rs` now looks the mod up before its file for that reason.

The account's setting comes from `preferences { adult }` on the GraphQL endpoint,
which is where Nexus keeps it - `users/validate` does not carry it. It is cached
in `nexus.ini` for a day and forgotten on sign-out. Everything fails closed: no
session, an unreadable preference, a stale cache, or a payload with no rating at
all all hide the mod, each with its own message so "we could not check" is never
confused with "you turned it off". `version` survives redaction, because comparing
it is what the update check does and a version string describes nothing - a user
with an adult mod already installed keeps seeing that an update exists.

**The request budget.** `get` and `send_with_version` now begin with a pre-flight
check, so the client stops as soon as `X-RL-Hourly-Remaining` or
`X-RL-Daily-Remaining` reaches 0 instead of waiting to be told 429. Either counter
is enough, which is stricter than Nexus's own documented rule (both buckets must
be spent) and is what was asked for. The block lifts on the UTC boundary the docs
guarantee, so no probe request is needed and no date parser either. Unknown always
means "go ahead": a client that has seen no headers yet must be able to send the
request that teaches it the budget.

Three bugs found on the way, each of which would have defeated the fix:

- `capture_limits` rebuilt the whole struct from every reply, so an answer with no
  `X-RL-*` headers - a 401 carries none - wiped a known-exhausted budget back to
  unknown and the next request went straight out.
- the loops disagreed on what a rate-limit error looks like: the library matched
  "rate limited", the CLI matched "429". A pre-flight refusal carries no status
  code, so it would have stopped one and left the other hammering. Both now share
  `is_rate_limited`.
- a 429 arriving while the counters still showed budget is the burst guard, not
  the quota; it now backs off for a minute instead of idling until the next hour.

Unavailable mods are withheld through the same path, matching what Vortex filters.
Adult mods coming back blank with no explanation reads as Eidos being broken.
The Nexus panel now states which of the three cases applies, and the one the
user can act on - "Eidos could not read your content settings" - is worded
differently from "you turned it off", because the fixes are different.

Read from the credential store rather than plumbed through app state: that
store is what the client itself consults, and a second copy in the UI could
end up disagreeing with what is actually being withheld.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant