docs(design): add OKF user story catalog
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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.`);
|
||||
Reference in New Issue
Block a user