Skip to content

feat(links): link external accounts to players with one-time codes - #184

Open
TheMeinerLP wants to merge 8 commits into
mainfrom
feat/account-links
Open

TheMeinerLP wants to merge 8 commits into
mainfrom
feat/account-links

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Summary

Otis becomes the registry for links between Minecraft players and external accounts (Discord, Twitch, YouTube, X, TikTok, GitHub).

  • Verified links come only from a one-time link code:
    • The game side (proxy) requests a code: POST /v1/players/{playerUuid}/link-codes.
    • The redeeming service (e.g. the Discord bot, which authenticated the external user itself) redeems it: POST /v1/link-codes/redeem.
    • Codes: 8 chars Crockford Base32 (XXXX-XXXX), 10 min TTL, single use, stored only as SHA-256.
    • A new code revokes the previous open one. At most 5 codes per player per hour (429).
    • Unknown, expired, used and provider-mismatched codes all answer the same 410 link-code-invalid (no oracle).
    • Redeeming is atomic: on a conflict (409 external-account-already-linked / provider-already-linked) everything rolls back and the code stays usable.
  • Unverified public profile links: the player sets them via PUT /v1/players/{playerUuid}/links/{provider} (validated handle or https URL on the provider's domain). They never replace a verified link, and redeeming upgrades them.
  • Reading and removing links:
    • GET /v1/players/{uuid}/links lists them; DELETE .../{provider} is idempotent (204).
    • GET /v1/links/{provider}/{externalId} answers "who is this Discord user?" and only finds verified links.
  • Other:
    • No provider OAuth tokens are stored. Links and codes are deleted together with their player (cascade).
    • Spans links.* carry provider, player uuid and outcome; codes and external ids never appear in spans or logs.
    • The client gets AccountLinksApi in the hand-maintained spec 1.1.0.

Planned service authentication (follow-up)

Service auth will use the OneLiteFeather SSO with JWT bearer tokens (client credentials, Micronaut Security JWT against the IdP's JWKS). In preparation, the endpoints are split into two controllers, so a scope can later be applied per group without breaking the API:

  • PlayerLinkController: the game side
  • LinkRedemptionController: the redeeming services

Until then Otis must stay reachable only internally, because redeem and lookup trust the caller's externalId.

Compatibility and migration (no data loss)

  • Flyway V3 creates account_link and link_code (h2, postgresql, mariadb) and is additive only. Existing tables are untouched.
  • ExistingDatabaseMigrationTest checks that player and setting rows stay byte-identical across the migration. The V1-V3 scripts were applied to throwaway MariaDB 11 and PostgreSQL 17 containers.
  • Existing endpoints are unchanged (ExistingEndpointsCompatibilityTest is green). The client javap diff is additions only (+247 lines, 0 removed). velocity-plugin builds unchanged, and its shadow JAR has no net/kyori classes.

Notes

  • Transactions use TransactionOperations<Session> @Named("default"), which resolves to SpringHibernateTransactionOperations. Declarative @Transactional is ambiguous here (two @Primary beans). A rollback test proves that the repositories join the transaction.
  • Deviations are recorded in openspec/changes/add-account-links/design.md under "Implementation Notes":
    • The client class is named AccountLinksApi.
    • The rate-limit window is exclusive.
    • A malformed code gets the 410 without a DB lookup.
  • Stacked follow-ups: add-account-link-events (Kafka outbox) and add-link-commands (proxy commands).

Test plan

  • ./gradlew clean build: 318 tests, 0 failures, 1 skipped (pre-existing disabled OtisTest)
  • One API test per spec scenario (34), concurrent redeem test (virtual threads, no sleeps), rollback test, player-deletion cascade test
  • V1-V3 applied on MariaDB 11 and PostgreSQL 17 containers

https://claude.ai/code/session_012BmpdMdQ5wwagagQvfcq8S

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Test results

 99 files   99 suites   1m 13s ⏱️
317 tests 316 ✅ 1 💤 0 ❌
954 runs  951 ✅ 3 💤 0 ❌

Results for commit 0de1209.

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