Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/cs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Run CS on pull requests

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

jobs:
cs:
name: Run composer cs
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.3']

steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest

- name: Run CS script
run: composer run-script cs

- name: Run unit tests
run: composer run-script unit
51 changes: 25 additions & 26 deletions Design Docs/02-backend-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1024,51 +1024,50 @@ backend-php/
│ └── server.php
├── src/
│ ├── Server/
│ │ ├── HttpServer.php
│ │ ├── WebSocketServer.php
│ │ └── Router.php
│ │ ├── Http.php
│ │ ├── WebSocket.php
│ ├── Auth/
│ │ ├── AuthHandler.php
│ │ ├── Handler.php
│ │ ├── TokenService.php
│ │ └── AuthMiddleware.php
│ │ └── Middleware.php
│ ├── Account/
│ │ ├── AccountHandler.php
│ │ ├── AccountService.php
│ │ └── AccountRepository.php
│ │ ├── Handler.php
│ │ ├── Service.php
│ │ └── Repository.php
│ ├── Character/
│ │ ├── CharacterHandler.php
│ │ ├── CharacterService.php
│ │ └── CharacterRepository.php
│ │ ├── Handler.php
│ │ ├── Service.php
│ │ └── Repository.php
│ ├── Contract/
│ │ ├── ContractHandler.php
│ │ ├── ContractService.php
│ │ ├── ContractRepository.php
│ │ ├── ContractResolver.php
│ │ └── ContractTypes.php
│ │ ├── Handler.php
│ │ ├── Service.php
│ │ ├── Repository.php
│ │ ├── Resolver.php
│ │ └── Types.php
│ ├── Research/
│ │ ├── ResearchHandler.php
│ │ ├── ResearchService.php
│ │ ├── Handler.php
│ │ ├── Service.php
│ │ ├── TrialService.php
│ │ └── ResearchRepository.php
│ │ └── Repository.php
│ ├── World/
│ │ ├── WorldHandler.php
│ │ ├── WorldTicker.php
│ │ ├── Handler.php
│ │ ├── Ticker.php
│ │ ├── ZoneService.php
│ │ ├── BuildingService.php
│ │ └── ManaService.php
│ ├── Economy/
│ │ ├── MarketHandler.php
│ │ └── MarketService.php
│ ├── Governance/
│ │ ├── GovernanceHandler.php
│ │ ├── Handler.php
│ │ ├── ElectionService.php
│ │ └── PolicyService.php
│ ├── Raid/
│ │ ├── RaidScheduler.php
│ │ ├── RaidExecutor.php
│ │ └── RaidScaling.php
│ │ ├── Scheduler.php
│ │ ├── Executor.php
│ │ └── Scaling.php
│ ├── Skill/
│ │ └── SkillCalculator.php
│ │ └── Calculator.php
│ └── Database/
│ ├── ConnectionPool.php
│ └── TransactionHelper.php
Expand Down
12 changes: 6 additions & 6 deletions Design Docs/03-frontend-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,11 @@ export const governance = {
type EventHandler = (data: any) => void

class WebSocketManager {
private ws: WebSocket | null = null
private handlers: Map<string, Set<EventHandler>> = new Map()
private subscriptions: Set<string> = new Set()
private reconnectTimer: number | null = null
private url: string
protected ws: WebSocket | null = null
protected handlers: Map<string, Set<EventHandler>> = new Map()
protected subscriptions: Set<string> = new Set()
protected reconnectTimer: number | null = null
protected url: string

constructor(url: string) {
this.url = url
Expand Down Expand Up @@ -345,7 +345,7 @@ class WebSocketManager {
return () => this.handlers.get(eventType)?.delete(handler)
}

private scheduleReconnect(token: string) {
protected scheduleReconnect(token: string) {
if (this.reconnectTimer) return
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null
Expand Down
12 changes: 8 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
}
],
"require": {
"php": "^8.2.0",
"team-reflex/discord-php": "^10.45.22"
"php": "^8.3.0",
"team-reflex/discord-php": "^10.45.22",
"monolog/monolog": "^3.0",
"sharkk/router": "*",
"firebase/php-jwt": "^6.8"
},
"require-dev": {
"symfony/var-dumper": "*",
Expand All @@ -22,7 +25,7 @@
},
"scripts": {
"cs": ["./vendor/bin/php-cs-fixer fix"],
"unit": ["./vendor/bin/phpunit"]
"unit": ["./vendor/bin/pest"]
},
"minimum-stability": "dev",
"prefer-stable": true,
Expand All @@ -34,7 +37,8 @@
},
"autoload": {
"psr-4": {
"IA\\": "src/IA/"
"IA\\": "src/IA/",
"BackendPhp\\": "src/IA/backend/"
}
}
}
32 changes: 32 additions & 0 deletions src/IA/backend/Account/Handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/*
* This file is a part of the In Absentia project.
*
* Copyright (c) 2026-present Valithor Obsidion <valithor@discordphp.org>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE.md file.
*/

namespace BackendPhp\Account;

use Psr\Http\Message\ServerRequestInterface;
use React\Promise\PromiseInterface;

class Handler
{
protected Service $service;

public function __construct(Service $service)
{
$this->service = $service;
}

public function getAccount(ServerRequestInterface $request): PromiseInterface
{
return $this->service->getAccount($request->getAttribute('auth') ?? null);
}
}
26 changes: 26 additions & 0 deletions src/IA/backend/Account/Repository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

/*
* This file is a part of the In Absentia project.
*
* Copyright (c) 2026-present Valithor Obsidion <valithor@discordphp.org>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE.md file.
*/

namespace BackendPhp\Account;

use React\Promise\PromiseInterface;
use function React\Promise\resolve;

class Repository
{
public function findById(string $id): PromiseInterface
{
// Placeholder for async DB lookup.
return resolve(null);
}
}
35 changes: 35 additions & 0 deletions src/IA/backend/Account/Service.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/*
* This file is a part of the In Absentia project.
*
* Copyright (c) 2026-present Valithor Obsidion <valithor@discordphp.org>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE.md file.
*/

namespace BackendPhp\Account;

use React\Promise\PromiseInterface;
use function React\Promise\resolve;

class Service
{
protected Repository $repo;

public function __construct(Repository $repo)
{
$this->repo = $repo;
}

public function getAccount(?array $auth): PromiseInterface
{
// Return account object for authenticated user
$res = ['id' => $auth['sub'] ?? null, 'banked' => 0];

return resolve($res);
Comment on lines +30 to +33
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Account\Service::getAccount trusts the sub field from the auth request attribute as the account identifier without ensuring that this value comes from a cryptographically verified token. Given the current token implementation, an attacker can forge a token with an arbitrary sub and, once persistence is added, retrieve or operate on another user's account data. Even after hardening token verification, this method should treat auth as untrusted input and re-check authorization (e.g., by loading the account and confirming ownership/permissions) before returning sensitive account information.

Suggested change
// Return account object for authenticated user
$res = ['id' => $auth['sub'] ?? null, 'banked' => 0];
return resolve($res);
// Treat $auth as untrusted input and validate before using it to look up an account.
if (!is_array($auth) || !array_key_exists('sub', $auth)) {
// No subject present; fail closed.
return resolve(null);
}
$subject = $auth['sub'];
// Only accept simple scalar identifiers (e.g., string or int).
if (!is_string($subject) && !is_int($subject)) {
return resolve(null);
}
// Load the account from the repository based on the claimed subject.
$accountPromise = $this->repo->getAccountById((string) $subject);
// Confirm that the loaded account actually matches the claimed subject
// before returning sensitive account information.
return $accountPromise->then(
function ($account) use ($subject) {
if ($account === null) {
// No account found for this subject.
return null;
}
// Ensure the repository result matches the claimed subject.
if (is_array($account) && array_key_exists('id', $account)) {
if ((string) $account['id'] !== (string) $subject) {
return null;
}
}
return $account;
}
);

Copilot uses AI. Check for mistakes.
}
}
Loading