diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c405d4b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Repository Agent Guidance + +## User-story-driven development + +The `design/` directory is the OKF v0.1 product record for this repository. Use user stories to plan, implement, verify, and track all behavior. + +Before changing behavior: + +1. Read `design/index.md` and every story related to the requested behavior. +2. Update an existing story or create a new `design/us-NNN-short-name.md` story before implementation. +3. Define observable acceptance criteria using user or operator language. +4. Set story status to `proposed` or `in-progress` while the work is incomplete. + +While implementing: + +1. Work in vertical slices against the documented acceptance criteria. +2. Add tests for important behavior before implementation when practical. +3. Keep implementation references and related-story links current. +4. Do not mark an acceptance criterion complete until the behavior exists and has been validated. + +Before completing or committing: + +1. Set completed story status to `implemented` or `verified` as appropriate. +2. Check completed acceptance criteria and record validation evidence. +3. Update `design/index.md` whenever stories are added, renamed, moved, or materially reclassified. +4. Add a high-level entry to `design/log.md` under the verified current date. +5. Run `npm run design:validate` along with relevant tests, type checks, lint, and builds. + +## OKF conventions + +- Every non-reserved Markdown file in `design/` must have YAML frontmatter with a non-empty `type`. +- User stories use `type: User Story` and include `story_id`, `status`, `title`, `description`, `tags`, and `timestamp`. +- Allowed story statuses are `proposed`, `in-progress`, `implemented`, and `verified`. +- `design/index.md` and `design/log.md` are reserved OKF files and follow the OKF index/log structures. +- Prefer structured sections: `# User Story`, `# Acceptance Criteria`, `# Implementation`, `# Validation`, and `# Related Stories`. +- Use repository-relative links and keep them valid when files move. +- Preserve unknown frontmatter extensions. + +## Timestamps + +Always run `date -u +%Y-%m-%dT%H:%M:%SZ` before adding or updating story timestamps or dated log entries. Never guess dates. diff --git a/README.md b/README.md index 356ddfd..8c2d2d9 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,14 @@ Set `IP_INTELLIGENCE_PROVIDER=proxycheck`, add `PROXYCHECK_API_KEY`, and configu Open `http://localhost:3000`. +## Product design + +Implemented and proposed behavior is tracked as OKF user stories in [`design/index.md`](design/index.md). Validate the bundle with `npm run design:validate`. + ## Validation ```bash +npm run design:validate npm test npm run typecheck npm run lint diff --git a/design/index.md b/design/index.md new file mode 100644 index 0000000..072d457 --- /dev/null +++ b/design/index.md @@ -0,0 +1,35 @@ +--- +okf_version: "0.1" +--- + +# Minecraft Account Manager User Stories + +This OKF bundle is the product record for implemented and proposed behavior. Story status and acceptance criteria are maintained alongside code changes. + +## Player Experience + +* [US-001 — Enter through Discord](us-001-discord-entry.md) - Direct portal visitors are guided to the configured Discord server. +* [US-002 — Authenticate with a Discord magic link](us-002-discord-magic-link.md) - Discord users receive secure, private, single-use portal links. +* [US-003 — Complete first-time onboarding](us-003-onboarding.md) - New users provide a name, connect a Java account, and confirm Discord identity. +* [US-004 — Validate Minecraft accounts](us-004-minecraft-validation.md) - Java usernames resolve through Mojang with explicit unverified overrides. +* [US-005 — Manage linked accounts](us-005-user-dashboard.md) - Users manage names, accounts, primaries, and security history. +* [US-006 — Keep Discord nicknames synchronized](us-006-discord-nickname.md) - Names and primary accounts determine the guild nickname. + +## Network and Game Access + +* [US-007 — Enrich login IPs](us-007-ip-intelligence.md) - Portal and game login events include cached ProxyCheck location and network data. +* [US-008 — Block anonymized account additions](us-008-vpn-blocking.md) - VPN, proxy, Tor, and unknown networks cannot add accounts. +* [US-009 — Enforce registration at Velocity](us-009-velocity-admission.md) - The proxy admits positively identified registered Java accounts only. + +## Administration and Governance + +* [US-010 — Preserve an audit trail](us-010-audit-events.md) - Security and account activity is stored as CloudEvents-style events. +* [US-011 — Authenticate administrators with SSO](us-011-admin-sso.md) - Keycloak and a required role protect the operator console. +* [US-012 — Operate settings and audit views](us-012-admin-operations.md) - Administrators configure denial messaging and inspect events. +* [US-013 — Manage users as an administrator](us-013-admin-user-management.md) - Administrators search users and manage names and Minecraft accounts. +* [US-014 — Receive standardized API errors](us-014-problem-details.md) - Application APIs return RFC 9457 Problem Details. +* [US-015 — Deploy and operate securely](us-015-platform-operations.md) - Operators have reproducible builds, migrations, credentials, and security controls. + +# Tracking + +See the [design update log](log.md) for high-level changes. New work starts by creating or updating a story and its acceptance criteria. diff --git a/design/log.md b/design/log.md new file mode 100644 index 0000000..77ec53a --- /dev/null +++ b/design/log.md @@ -0,0 +1,7 @@ +# Design Update Log + +## 2026-08-01 + +* **Create**: Established the OKF v0.1 [user-story index](index.md). +* **Document**: Captured the implemented player portal, Discord authentication, onboarding, account management, network intelligence, Velocity admission, auditing, administration, API error, and operational stories. +* **Governance**: Added repository agent guidance and automated OKF validation for story-driven development. diff --git a/design/us-001-discord-entry.md b/design/us-001-discord-entry.md new file mode 100644 index 0000000..88416c6 --- /dev/null +++ b/design/us-001-discord-entry.md @@ -0,0 +1,35 @@ +--- +type: User Story +title: Enter the account portal through Discord +description: Direct visitors are guided to the configured Discord community and its account commands. +tags: [player, portal, discord, onboarding] +timestamp: 2026-08-01T18:43:58Z +story_id: US-001 +status: verified +--- + +# User Story + +As a prospective player, I want the portal to direct me to the community Discord, so that I can begin registration through the trusted entry point. + +# Acceptance Criteria + +- [x] Given an unauthenticated visitor, when they open the portal, then they are told to run `/register` or `/account` in Discord. +- [x] Given a configured invite URL, when the visitor selects the join action, then the Discord invite opens in a new browser context. +- [x] Given a configured guild ID, when the visitor selects the app action, then a `discord://` guild link is opened. +- [x] Given an unauthenticated protected-page request, when authorization fails, then the visitor returns to the portal with prominent Discord instructions. + +# Implementation + +- [`apps/web/src/app/page.tsx`](../apps/web/src/app/page.tsx) +- [`apps/web/src/lib/auth/user-session.ts`](../apps/web/src/lib/auth/user-session.ts) +- Configuration: `DISCORD_GUILD_ID`, `DISCORD_INVITE_URL` + +# Validation + +Covered by the Next.js production build and protected-route session checks. + +# Related Stories + +- [Discord magic-link authentication](us-002-discord-magic-link.md) +- [First-time onboarding](us-003-onboarding.md) diff --git a/design/us-002-discord-magic-link.md b/design/us-002-discord-magic-link.md new file mode 100644 index 0000000..716d23e --- /dev/null +++ b/design/us-002-discord-magic-link.md @@ -0,0 +1,39 @@ +--- +type: User Story +title: Authenticate with a Discord magic link +description: Discord users receive private single-use links that establish secure portal sessions. +tags: [player, discord, authentication, security] +timestamp: 2026-08-01T18:43:58Z +story_id: US-002 +status: verified +--- + +# User Story + +As a Discord community member, I want `/register` and `/account` to issue a private sign-in link, so that I can access the portal without creating another password. + +# Acceptance Criteria + +- [x] Given the configured guild, when a user runs `/register` or `/account`, then the bot responds ephemerally with a private link. +- [x] Given a generated link, then the raw login token is never stored in PostgreSQL. +- [x] Given a login token, then it expires after ten minutes and can be consumed only once. +- [x] Given repeated link requests, then requests are rate limited per Discord user and older active links are invalidated. +- [x] Given a valid link, when it is consumed, then the Discord user is created or refreshed and a secure seven-day session is established. +- [x] Given an invalid, expired, or consumed link, then the user sees a safe recovery page instructing them to request another link. + +# Implementation + +- [`apps/discord-bot/src/index.ts`](../apps/discord-bot/src/index.ts) +- [`packages/auth/src/index.ts`](../packages/auth/src/index.ts) +- [`packages/database/src/auth-repository.ts`](../packages/database/src/auth-repository.ts) +- [`apps/web/src/app/auth/discord/route.ts`](../apps/web/src/app/auth/discord/route.ts) + +# Validation + +- [`packages/auth/test/magic-link.test.ts`](../packages/auth/test/magic-link.test.ts) +- Discord command and authentication workspaces pass TypeScript validation. + +# Related Stories + +- [Enter through Discord](us-001-discord-entry.md) +- [Preserve an audit trail](us-010-audit-events.md) diff --git a/design/us-003-onboarding.md b/design/us-003-onboarding.md new file mode 100644 index 0000000..8346706 --- /dev/null +++ b/design/us-003-onboarding.md @@ -0,0 +1,39 @@ +--- +type: User Story +title: Complete first-time onboarding +description: New users establish their preferred identity and first Minecraft account. +tags: [player, onboarding, minecraft, discord] +timestamp: 2026-08-01T18:43:58Z +story_id: US-003 +status: verified +--- + +# User Story + +As a newly authenticated player, I want a guided setup flow, so that my preferred name, Minecraft identity, and Discord nickname are configured correctly. + +# Acceptance Criteria + +- [x] Given a new Discord user, when they enter the portal, then they receive a personalized welcome. +- [x] Given the first onboarding step, when the user enters a valid preferred name, then it is stored for their profile. +- [x] Given the Minecraft step, when a valid Java username is submitted from an allowed network, then it is verified and added as primary. +- [x] Given an unverifiable but syntactically valid username, then the user must explicitly confirm before continuing. +- [x] Given a name and primary account, then the expected Discord nickname is previewed before any guild update. +- [x] Given confirmation and a successful Discord update, then onboarding is marked complete and the dashboard opens. + +# Implementation + +- [`apps/web/src/app/welcome/page.tsx`](../apps/web/src/app/welcome/page.tsx) +- [`apps/web/src/app/welcome/minecraft/page.tsx`](../apps/web/src/app/welcome/minecraft/page.tsx) +- [`apps/web/src/app/welcome/discord/page.tsx`](../apps/web/src/app/welcome/discord/page.tsx) +- [`apps/web/src/app/welcome/actions.ts`](../apps/web/src/app/welcome/actions.ts) + +# Validation + +Onboarding routes are protected by database-backed sessions and included in production route generation. + +# Related Stories + +- [Validate Minecraft accounts](us-004-minecraft-validation.md) +- [Synchronize Discord nicknames](us-006-discord-nickname.md) +- [Block anonymized account additions](us-008-vpn-blocking.md) diff --git a/design/us-004-minecraft-validation.md b/design/us-004-minecraft-validation.md new file mode 100644 index 0000000..2eb3b59 --- /dev/null +++ b/design/us-004-minecraft-validation.md @@ -0,0 +1,38 @@ +--- +type: User Story +title: Validate Minecraft accounts +description: Java Edition usernames resolve to canonical Mojang identities with controlled override behavior. +tags: [player, minecraft, mojang, identity] +timestamp: 2026-08-01T18:43:58Z +story_id: US-004 +status: verified +--- + +# User Story + +As a player, I want submitted Minecraft usernames checked against Mojang, so that the server can identify my online-mode Java account reliably. + +# Acceptance Criteria + +- [x] Given a syntactically valid username, when it is submitted, then validation occurs server-side against the fixed Mojang endpoint. +- [x] Given a Mojang match, then the canonical username and compact UUID are stored. +- [x] Given no Mojang match, then the user or administrator must explicitly confirm an unverified override. +- [x] Given malformed input, then it cannot be stored even through an override. +- [x] Given an active UUID or case-insensitive username already registered, then another active registration is rejected. +- [x] Given a later online-mode game login for an unverified account, then its UUID can be safely backfilled after username matching. + +# Implementation + +- [`packages/minecraft/src/index.ts`](../packages/minecraft/src/index.ts) +- [`packages/database/src/schema.ts`](../packages/database/src/schema.ts) +- User and administrator account actions under [`apps/web/src/app`](../apps/web/src/app) + +# Validation + +- [`packages/minecraft/test/minecraft.test.ts`](../packages/minecraft/test/minecraft.test.ts) +- Database partial unique indexes preserve active identity invariants. + +# Related Stories + +- [First-time onboarding](us-003-onboarding.md) +- [Velocity game admission](us-009-velocity-admission.md) diff --git a/design/us-005-user-dashboard.md b/design/us-005-user-dashboard.md new file mode 100644 index 0000000..7431f5f --- /dev/null +++ b/design/us-005-user-dashboard.md @@ -0,0 +1,40 @@ +--- +type: User Story +title: Manage linked accounts from the dashboard +description: Authenticated users maintain their profile and active Java Edition accounts. +tags: [player, dashboard, minecraft, profile] +timestamp: 2026-08-01T18:43:58Z +story_id: US-005 +status: verified +--- + +# User Story + +As a registered player, I want to manage my profile and linked Minecraft accounts, so that my whitelist identity remains current. + +# Acceptance Criteria + +- [x] Given an authenticated user, then only their own profile, accounts, and IP observations are visible and mutable. +- [x] The user can update their preferred name. +- [x] The user can add Mojang-verified or explicitly confirmed accounts from an allowed network. +- [x] The user can soft-remove an active account. +- [x] The user can choose exactly one active primary account. +- [x] Removing a primary account promotes another active account when one exists. +- [x] Name and primary changes show the expected Discord nickname and require confirmation. +- [x] The dashboard shows recent portal and game IP observations with classification and available location. +- [x] The user can revoke the current session by signing out. + +# Implementation + +- [`apps/web/src/app/account/page.tsx`](../apps/web/src/app/account/page.tsx) +- [`apps/web/src/app/account/actions.ts`](../apps/web/src/app/account/actions.ts) +- [`apps/web/src/app/auth/actions.ts`](../apps/web/src/app/auth/actions.ts) + +# Validation + +Server actions verify the current session and constrain every account lookup by the authenticated user ID. + +# Related Stories + +- [Synchronize Discord nicknames](us-006-discord-nickname.md) +- [Enrich login IPs](us-007-ip-intelligence.md) diff --git a/design/us-006-discord-nickname.md b/design/us-006-discord-nickname.md new file mode 100644 index 0000000..bf143ea --- /dev/null +++ b/design/us-006-discord-nickname.md @@ -0,0 +1,39 @@ +--- +type: User Story +title: Keep Discord nicknames synchronized +description: Preferred names and primary Minecraft usernames determine community guild nicknames. +tags: [player, admin, discord, identity] +timestamp: 2026-08-01T18:43:58Z +story_id: US-006 +status: verified +--- + +# User Story + +As a community member, I want my Discord nickname to reflect my preferred name and primary Minecraft account, so that other players can identify me consistently. + +# Acceptance Criteria + +- [x] Given a preferred name and primary account, then the nickname format is `First name (MinecraftUsername)`. +- [x] Given Discord's 32-character limit, then the preferred-name portion is shortened while preserving the Minecraft username. +- [x] Given no remaining Minecraft account, then administrative synchronization falls back to the preferred name. +- [x] User name and primary changes display the proposed nickname before confirmation. +- [x] Administrator name, primary, and primary-removal operations synchronize the nickname automatically. +- [x] Discord failures are reported without falsely claiming the requested profile change completed. +- [x] A protected administrative retry action can synchronize the current desired nickname. + +# Implementation + +- [`packages/minecraft/src/index.ts`](../packages/minecraft/src/index.ts) +- [`apps/web/src/app/account/actions.ts`](../apps/web/src/app/account/actions.ts) +- [`apps/web/src/app/admin/(console)/users/actions.ts`](../apps/web/src/app/admin/%28console%29/users/actions.ts) + +# Validation + +- [`packages/minecraft/test/discord.test.ts`](../packages/minecraft/test/discord.test.ts) +- Nickname length and fallback behavior are covered in [`packages/minecraft/test/minecraft.test.ts`](../packages/minecraft/test/minecraft.test.ts). + +# Related Stories + +- [Manage linked accounts](us-005-user-dashboard.md) +- [Administer users](us-013-admin-user-management.md) diff --git a/design/us-007-ip-intelligence.md b/design/us-007-ip-intelligence.md new file mode 100644 index 0000000..80b185b --- /dev/null +++ b/design/us-007-ip-intelligence.md @@ -0,0 +1,41 @@ +--- +type: User Story +title: Enrich portal and game login IPs +description: Login audit events include cached approximate location and network intelligence from ProxyCheck.io. +tags: [security, network, audit, proxycheck] +timestamp: 2026-08-01T18:43:58Z +story_id: US-007 +status: verified +--- + +# User Story + +As an operator, I want portal and registered game logins enriched with network context, so that suspicious access can be investigated. + +# Acceptance Criteria + +- [x] Given a public login IP, then ProxyCheck can provide city, region, country, coordinates, timezone, ASN, provider, risk, and anonymity classification. +- [x] Results are cached in PostgreSQL for 48 hours by default. +- [x] Provider failures are cached briefly and do not deny portal or registered game login. +- [x] Private, loopback, reserved, documentation, and mapped-private addresses are never sent to ProxyCheck. +- [x] Forwarded web IP headers are ignored unless trusted-proxy handling is explicitly enabled. +- [x] Unknown game accounts do not trigger paid ProxyCheck lookups. +- [x] Login events and IP observations retain the available classification and approximate location. +- [x] Users and administrators can see available location and classification in audit views. + +# Implementation + +- [`packages/network/src/index.ts`](../packages/network/src/index.ts) +- [`apps/web/src/lib/ip-intelligence.ts`](../apps/web/src/lib/ip-intelligence.ts) +- [`apps/web/src/app/auth/discord/route.ts`](../apps/web/src/app/auth/discord/route.ts) +- [`apps/web/src/app/api/velocity/access/route.ts`](../apps/web/src/app/api/velocity/access/route.ts) + +# Validation + +- [`packages/network/test/proxycheck.test.ts`](../packages/network/test/proxycheck.test.ts) +- [`packages/network/test/client-ip.test.ts`](../packages/network/test/client-ip.test.ts) + +# Related Stories + +- [Block anonymized additions](us-008-vpn-blocking.md) +- [Preserve an audit trail](us-010-audit-events.md) diff --git a/design/us-008-vpn-blocking.md b/design/us-008-vpn-blocking.md new file mode 100644 index 0000000..2dfddf1 --- /dev/null +++ b/design/us-008-vpn-blocking.md @@ -0,0 +1,39 @@ +--- +type: User Story +title: Block account additions from anonymized networks +description: User Minecraft-account additions fail closed for VPN, proxy, Tor, or unknown IP classifications. +tags: [security, vpn, proxy, minecraft] +timestamp: 2026-08-01T18:43:58Z +story_id: US-008 +status: verified +--- + +# User Story + +As an operator, I want account additions blocked from anonymized networks, so that whitelist identities are established from attributable connections. + +# Acceptance Criteria + +- [x] VPN, proxy, and Tor classifications block user account additions. +- [x] Unknown or unavailable classification blocks additions rather than failing open. +- [x] Hosting-provider ranges can be blocked through deployment configuration. +- [x] Normal portal use and game login are not denied solely because intelligence is unavailable. +- [x] Blocked users receive a clear recovery message without provider internals. +- [x] Blocked and classification-unavailable attempts create distinct audit events with safe intelligence details. +- [x] Administrative account additions remain available as an authorized recovery path. + +# Implementation + +- [`apps/web/src/lib/ip-intelligence.ts`](../apps/web/src/lib/ip-intelligence.ts) +- [`apps/web/src/app/welcome/actions.ts`](../apps/web/src/app/welcome/actions.ts) +- [`apps/web/src/app/account/actions.ts`](../apps/web/src/app/account/actions.ts) +- Configuration: `PROXYCHECK_API_KEY`, `BLOCK_HOSTING_IPS`, `TRUST_PROXY` + +# Validation + +The fail-closed classification policy and provider mappings are covered by [`packages/network/test/proxycheck.test.ts`](../packages/network/test/proxycheck.test.ts). + +# Related Stories + +- [Enrich login IPs](us-007-ip-intelligence.md) +- [Validate Minecraft accounts](us-004-minecraft-validation.md) diff --git a/design/us-009-velocity-admission.md b/design/us-009-velocity-admission.md new file mode 100644 index 0000000..6a36441 --- /dev/null +++ b/design/us-009-velocity-admission.md @@ -0,0 +1,43 @@ +--- +type: User Story +title: Enforce registration at the Velocity proxy +description: Online-mode Java connections are admitted only after a fail-closed account-manager decision. +tags: [minecraft, velocity, whitelist, security] +timestamp: 2026-08-01T18:43:58Z +story_id: US-009 +status: verified +--- + +# User Story + +As a registered player, I want the Velocity proxy to recognize my approved Java account, so that I can join while unknown identities are rejected. + +# Acceptance Criteria + +- [x] The plugin sends request ID, server ID, online-mode UUID, username, IP, and occurrence time. +- [x] Every request uses a high-entropy per-server bearer credential stored only as a hash by the service. +- [x] Requests outside the 45-second clock window are rejected. +- [x] Database-unique request IDs reject cross-instance replay attempts. +- [x] UUID matching is attempted before username fallback. +- [x] Username fallback applies only when the stored account has no UUID. +- [x] Successful fallback backfills UUID and canonical username. +- [x] Changed usernames are persisted and audited. +- [x] Unknown players, API failures, malformed responses, and unauthorized requests fail closed with registration guidance. +- [x] The plugin records the real Velocity connection IP and supports Java Edition online mode only. + +# Implementation + +- [`plugins/velocity`](../plugins/velocity) +- [`apps/web/src/app/api/velocity/access/route.ts`](../apps/web/src/app/api/velocity/access/route.ts) +- [`packages/contracts/src/index.ts`](../packages/contracts/src/index.ts) +- [`packages/database/src/schema.ts`](../packages/database/src/schema.ts) + +# Validation + +- [`plugins/velocity/src/test/java/games/twentyfaces/accountmanager/AccountManagerClientTest.java`](../plugins/velocity/src/test/java/games/twentyfaces/accountmanager/AccountManagerClientTest.java) +- Shared request and response contracts are covered by [`packages/contracts/test/contracts.test.ts`](../packages/contracts/test/contracts.test.ts). + +# Related Stories + +- [Validate Minecraft accounts](us-004-minecraft-validation.md) +- [Standardize API errors](us-014-problem-details.md) diff --git a/design/us-010-audit-events.md b/design/us-010-audit-events.md new file mode 100644 index 0000000..50fb156 --- /dev/null +++ b/design/us-010-audit-events.md @@ -0,0 +1,39 @@ +--- +type: User Story +title: Preserve a CloudEvents-style audit trail +description: Authentication, UI, account, Discord, network, and game actions create searchable immutable-style events. +tags: [audit, cloudevents, security, events] +timestamp: 2026-08-01T18:43:58Z +story_id: US-010 +status: verified +--- + +# User Story + +As an operator, I want security and identity activity recorded consistently, so that incidents and account changes can be reconstructed and later published to Kafka. + +# Acceptance Criteria + +- [x] Events preserve CloudEvents-style ID, specification version, source, type, subject, time, content type, and JSON data. +- [x] Events can include user actor, IP address, and correlation ID. +- [x] Portal access, magic-link creation and consumption, account changes, nickname changes, VPN blocks, and game decisions are recorded. +- [x] Username changes learned from Velocity create their own event. +- [x] Administrative actions include the acting SSO identity in event data. +- [x] Events can be inspected globally and from an individual admin user view. +- [x] `published_at` reserves an outbox path for future Kafka publishing. + +# Implementation + +- [`packages/database/src/events.ts`](../packages/database/src/events.ts) +- [`packages/database/src/schema.ts`](../packages/database/src/schema.ts) +- [`apps/web/src/lib/audit.ts`](../apps/web/src/lib/audit.ts) +- [`apps/web/src/app/admin/(console)/events/page.tsx`](../apps/web/src/app/admin/%28console%29/events/page.tsx) + +# Validation + +The shared CloudEvent contract is covered by [`packages/contracts/test/contracts.test.ts`](../packages/contracts/test/contracts.test.ts), and event-producing routes pass full type and production-build validation. + +# Related Stories + +- [Enrich login IPs](us-007-ip-intelligence.md) +- [Administer users](us-013-admin-user-management.md) diff --git a/design/us-011-admin-sso.md b/design/us-011-admin-sso.md new file mode 100644 index 0000000..8d474f0 --- /dev/null +++ b/design/us-011-admin-sso.md @@ -0,0 +1,39 @@ +--- +type: User Story +title: Authenticate administrators with Keycloak SSO +description: The operator console requires a Keycloak identity with the configured administrator role. +tags: [admin, keycloak, oidc, authentication] +timestamp: 2026-08-01T18:43:58Z +story_id: US-011 +status: verified +--- + +# User Story + +As an administrator, I want to authenticate through organizational SSO, so that privileged operations use centrally managed identities and roles. + +# Acceptance Criteria + +- [x] Admin authentication uses Keycloak OpenID Connect authorization code flow. +- [x] Sign-in is denied when the configured required role is absent. +- [x] Realm and configured-client roles are extracted from fresh Keycloak tokens. +- [x] Admin console layouts redirect unauthenticated or unauthorized users to the admin login page. +- [x] Every privileged server action independently rechecks the admin session and role. +- [x] Admin sessions use signed JWT behavior managed by NextAuth. +- [x] Administrators can sign out and return to the restricted login page. + +# Implementation + +- [`apps/web/src/lib/auth/admin-auth.ts`](../apps/web/src/lib/auth/admin-auth.ts) +- [`apps/web/src/lib/auth/require-admin.ts`](../apps/web/src/lib/auth/require-admin.ts) +- [`apps/web/src/app/admin`](../apps/web/src/app/admin) +- [`docs/admin-oidc-keycloak-setup.md`](../docs/admin-oidc-keycloak-setup.md) + +# Validation + +OIDC role extraction is covered by [`packages/auth/test/oidc-roles.test.ts`](../packages/auth/test/oidc-roles.test.ts). + +# Related Stories + +- [Operate settings and audit views](us-012-admin-operations.md) +- [Administer users](us-013-admin-user-management.md) diff --git a/design/us-012-admin-operations.md b/design/us-012-admin-operations.md new file mode 100644 index 0000000..4e5f995 --- /dev/null +++ b/design/us-012-admin-operations.md @@ -0,0 +1,38 @@ +--- +type: User Story +title: Operate settings and audit views +description: Authorized administrators control server messaging and investigate recent platform events. +tags: [admin, settings, audit, operations] +timestamp: 2026-08-01T18:43:58Z +story_id: US-012 +status: verified +--- + +# User Story + +As an administrator, I want operational settings and audit visibility, so that I can manage player guidance and investigate activity. + +# Acceptance Criteria + +- [x] The admin console reports whether deployment-managed Discord guild and invite settings are configured. +- [x] An authorized administrator can update the denied-player registration message. +- [x] Settings actions validate message length server-side. +- [x] Administrators can browse the latest 100 events. +- [x] Event views show type, subject, IP, classification, and approximate location when available. +- [x] Admin console access itself creates an audit event with the SSO identity. +- [x] Settings, users, and events are linked from the shared admin navigation. + +# Implementation + +- [`apps/web/src/app/admin/(console)/page.tsx`](../apps/web/src/app/admin/%28console%29/page.tsx) +- [`apps/web/src/app/admin/(console)/actions.ts`](../apps/web/src/app/admin/%28console%29/actions.ts) +- [`apps/web/src/app/admin/(console)/events/page.tsx`](../apps/web/src/app/admin/%28console%29/events/page.tsx) + +# Validation + +Admin routes are dynamic, role-protected, linted, and included in every production build. + +# Related Stories + +- [Administrator SSO](us-011-admin-sso.md) +- [Preserve an audit trail](us-010-audit-events.md) diff --git a/design/us-013-admin-user-management.md b/design/us-013-admin-user-management.md new file mode 100644 index 0000000..cf767a6 --- /dev/null +++ b/design/us-013-admin-user-management.md @@ -0,0 +1,41 @@ +--- +type: User Story +title: Manage users as an administrator +description: Authorized operators search users and maintain their names, linked accounts, primaries, and Discord nicknames. +tags: [admin, users, minecraft, discord] +timestamp: 2026-08-01T18:43:58Z +story_id: US-013 +status: verified +--- + +# User Story + +As an administrator, I want to manage a user's identity and Minecraft accounts, so that support issues can be resolved without direct database access. + +# Acceptance Criteria + +- [x] Administrators can search by preferred name, Discord username or ID, Minecraft username, or UUID. +- [x] Search results show onboarding state, primary username, and active account count. +- [x] A user detail view shows Discord identity, active accounts, recent events, and recent IP observations. +- [x] Administrators can update the preferred name and synchronize Discord. +- [x] Administrators can add Mojang-verified accounts or explicitly override an unverified username. +- [x] Administrators can remove an account only after a visible confirmation step. +- [x] Removing a primary account selects a replacement or falls back to the preferred-name nickname. +- [x] Administrators can set a new primary account and automatically update Discord. +- [x] Every action rechecks role and account ownership and records the acting administrator. +- [x] Discord failures do not falsely persist the requested name, primary, or removal change. + +# Implementation + +- [`apps/web/src/app/admin/(console)/users/page.tsx`](../apps/web/src/app/admin/%28console%29/users/page.tsx) +- [`apps/web/src/app/admin/(console)/users/[userId]/page.tsx`](../apps/web/src/app/admin/%28console%29/users/%5BuserId%5D/page.tsx) +- [`apps/web/src/app/admin/(console)/users/actions.ts`](../apps/web/src/app/admin/%28console%29/users/actions.ts) + +# Validation + +Nickname fallback behavior is tested in [`packages/minecraft/test/minecraft.test.ts`](../packages/minecraft/test/minecraft.test.ts). Privileged routes pass TypeScript, lint, Semgrep, and production build checks. + +# Related Stories + +- [Administrator SSO](us-011-admin-sso.md) +- [Synchronize Discord nicknames](us-006-discord-nickname.md) diff --git a/design/us-014-problem-details.md b/design/us-014-problem-details.md new file mode 100644 index 0000000..ed38292 --- /dev/null +++ b/design/us-014-problem-details.md @@ -0,0 +1,45 @@ +--- +type: User Story +title: Receive standardized API errors +description: Application-owned HTTP APIs expose RFC 9457 Problem Details matching game-ingest-server conventions. +tags: [api, errors, rfc9457, contracts] +timestamp: 2026-08-01T18:43:58Z +story_id: US-014 +status: verified +--- + +# User Story + +As an API consumer, I want errors returned as standardized Problem Details, so that failures can be handled consistently across game services. + +# Acceptance Criteria + +- [x] Error responses use `application/problem+json`. +- [x] Responses require `type`, `title`, and `status` and optionally include `detail`, `instance`, and `extensions`. +- [x] Invalid Velocity payloads include machine-readable Zod issues under `extensions.issues`. +- [x] Missing credentials, expired requests, replays, unsupported media, unsupported methods, unknown routes, and service failures have stable `urn:error:*` types. +- [x] Unexpected application-owned Velocity errors are converted to safe `503` problems without internal details. +- [x] A normal whitelist denial remains a successful `200` authorization decision. +- [x] Browser form redirects remain accessible HTML flows and OAuth protocol responses remain owned by NextAuth. + +# Implementation + +- [`packages/contracts/src/index.ts`](../packages/contracts/src/index.ts) +- [`apps/web/src/lib/problem-response.ts`](../apps/web/src/lib/problem-response.ts) +- [`apps/web/src/app/api/velocity/access/route.ts`](../apps/web/src/app/api/velocity/access/route.ts) +- [`docs/api-errors.md`](../docs/api-errors.md) + +# Validation + +- [`packages/contracts/test/problem-details.test.ts`](../packages/contracts/test/problem-details.test.ts) +- [`apps/web/src/app/api/velocity/access/route.test.ts`](../apps/web/src/app/api/velocity/access/route.test.ts) +- Unknown-route and response-helper tests in the web workspace. + +# Related Stories + +- [Velocity game admission](us-009-velocity-admission.md) +- [Deploy and operate securely](us-015-platform-operations.md) + +# Citations + +[1] [RFC 9457 — Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457) diff --git a/design/us-015-platform-operations.md b/design/us-015-platform-operations.md new file mode 100644 index 0000000..6f2d8ea --- /dev/null +++ b/design/us-015-platform-operations.md @@ -0,0 +1,43 @@ +--- +type: User Story +title: Deploy and operate the platform securely +description: Operators have repeatable builds, migrations, credential provisioning, configuration, and security checks. +tags: [operations, security, database, deployment] +timestamp: 2026-08-01T18:43:58Z +story_id: US-015 +status: verified +--- + +# User Story + +As a platform operator, I want reproducible deployment and security controls, so that the portal, bot, database, and proxy can be operated safely. + +# Acceptance Criteria + +- [x] The repository is an npm TypeScript workspace with separate web, bot, contract, database, network, and Minecraft modules. +- [x] PostgreSQL is available through Docker Compose for local use. +- [x] Drizzle changes use generated, versioned migrations rather than schema push. +- [x] Velocity credentials can be provisioned or rotated with a one-time-displayed token stored only as a hash. +- [x] The Velocity Gradle wrapper produces a tested shaded JAR. +- [x] Environment examples document database, Keycloak, Discord, trusted proxy, and ProxyCheck settings without secrets. +- [x] The web application sets CSP, framing, MIME, referrer, and permissions headers. +- [x] npm dependency audit and Semgrep security review complete without findings at the last verified change. +- [x] Architecture, Keycloak, API error, security, bot, and Velocity operating documentation is available. + +# Implementation + +- [`package.json`](../package.json) +- [`compose.yml`](../compose.yml) +- [`packages/database/drizzle`](../packages/database/drizzle) +- [`packages/database/scripts/create-plugin-credential.ts`](../packages/database/scripts/create-plugin-credential.ts) +- [`plugins/velocity/build.gradle.kts`](../plugins/velocity/build.gradle.kts) +- [`apps/web/next.config.ts`](../apps/web/next.config.ts) + +# Validation + +Use `npm test`, `npm run typecheck`, `npm run lint`, `npm run build`, `npm run velocity:build`, `npm audit`, and `npm run design:validate`. + +# Related Stories + +- [Administrator SSO](us-011-admin-sso.md) +- [Standardize API errors](us-014-problem-details.md) diff --git a/package.json b/package.json index 12a1b0b..f58566d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "typecheck": "npm run typecheck --workspaces --if-present", "db:generate": "npm run db:generate --workspace @minecraft-account-manager/database", "db:migrate": "npm run db:migrate --workspace @minecraft-account-manager/database", - "velocity:build": "cd plugins/velocity && ./gradlew clean test shadowJar" + "velocity:build": "cd plugins/velocity && ./gradlew clean test shadowJar", + "design:validate": "node scripts/validate-okf.mjs" }, "overrides": { "esbuild": "0.25.12", diff --git a/scripts/validate-okf.mjs b/scripts/validate-okf.mjs new file mode 100644 index 0000000..525eb11 --- /dev/null +++ b/scripts/validate-okf.mjs @@ -0,0 +1,89 @@ +import { access, readFile, readdir } from "node:fs/promises"; + +const designDirectory = new URL("../design/", import.meta.url); +const reservedFiles = new Set(["index.md", "log.md"]); +const allowedStatuses = new Set(["proposed", "in-progress", "implemented", "verified"]); +const failures = []; +const storyIds = new Map(); + +async function markdownFiles(directory, relativeDirectory = "") { + const files = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + files.push(...await markdownFiles(new URL(`${entry.name}/`, directory), relativePath)); + } else if (entry.isFile() && entry.name.endsWith(".md")) { + files.push({ name: entry.name, relativePath, url: new URL(entry.name, directory) }); + } + } + return files; +} + +for (const file of await markdownFiles(designDirectory)) { + const contents = await readFile(file.url, "utf8"); + for (const link of contents.matchAll(/\[[^\]]+\]\(([^)\s]+)\)/g)) { + const target = link[1].split("#", 1)[0]; + if (!target || target.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue; + const targetUrl = target.startsWith("/") + ? new URL(target.slice(1), designDirectory) + : new URL(target, file.url); + try { + await access(targetUrl); + } catch { + failures.push(`${file.relativePath}: broken link ${target}`); + } + } + + if (reservedFiles.has(file.name)) continue; + + const frontmatter = contents.match(/^---\n([\s\S]*?)\n---\n/); + if (!frontmatter) { + failures.push(`${file.relativePath}: missing YAML frontmatter`); + continue; + } + + const metadata = Object.fromEntries( + frontmatter[1] + .split("\n") + .map((line) => line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/)) + .filter(Boolean) + .map((match) => [match[1], match[2].replace(/^['"]|['"]$/g, "")]), + ); + + if (!metadata.type) failures.push(`${file.relativePath}: missing required type`); + if (metadata.type === "User Story") { + for (const field of ["story_id", "title", "description", "status", "timestamp"]) { + if (!metadata[field]) failures.push(`${file.relativePath}: missing ${field}`); + } + if (metadata.status && !allowedStatuses.has(metadata.status)) { + failures.push(`${file.relativePath}: invalid status ${metadata.status}`); + } + if (metadata.timestamp && Number.isNaN(Date.parse(metadata.timestamp))) { + failures.push(`${file.relativePath}: timestamp is not ISO 8601`); + } + if (metadata.story_id) { + const duplicate = storyIds.get(metadata.story_id); + if (duplicate) failures.push(`${file.relativePath}: duplicate ${metadata.story_id} also used by ${duplicate}`); + storyIds.set(metadata.story_id, file.relativePath); + } + } +} + +const index = await readFile(new URL("index.md", designDirectory), "utf8"); +for (const [storyId, filename] of storyIds) { + if (!index.includes(`(${filename})`)) failures.push(`index.md: missing ${storyId} link to ${filename}`); +} + +const log = await readFile(new URL("log.md", designDirectory), "utf8"); +for (const heading of log.matchAll(/^##\s+(.+)$/gm)) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(heading[1])) { + failures.push(`log.md: invalid date heading ${heading[1]}`); + } +} + +if (failures.length) { + console.error("OKF validation failed:\n" + failures.map((failure) => `- ${failure}`).join("\n")); + process.exit(1); +} + +console.log(`OKF validation passed: ${storyIds.size} user stories.`);