Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request lays the groundwork for a new backend architecture, establishing a robust and modular foundation for the project. It introduces core functionalities for user accounts, authentication, character management, and contract handling, alongside essential infrastructure for database interaction, dependency management, and server operations. The changes aim to create a scalable and maintainable system, supported by new utility classes and comprehensive testing. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new backend architecture with well-organized modules, clear separation of concerns, and the use of ReactPHP for asynchronous operations, complemented by a comprehensive test suite. However, a critical security vulnerability has been identified in the authentication system: the TokenService lacks cryptographic signatures, allowing for trivial token forgery and authentication bypass. Additionally, a hardcoded development token was found in the authentication handler. Beyond these critical security concerns, attention is also needed for improving dependency management, error handling, encapsulation, and avoiding global state.
src/IA/backend/Auth/TokenService.php
Outdated
| public function createToken(array $payload): string | ||
| { | ||
| return base64_encode(json_encode($payload)); | ||
| } | ||
|
|
||
| public function verifyToken(string $token): ?array | ||
| { | ||
| return json_decode(base64_decode($token), true) ?: null; | ||
| } | ||
| } |
There was a problem hiding this comment.
The TokenService implementation for createToken and verifyToken is critically insecure. It lacks cryptographic signatures, allowing an attacker to forge tokens by modifying the JSON payload (e.g., changing the sub field) and bypass authentication, enabling impersonation of any user. It is recommended to use a standard like JSON Web Tokens (JWT) with a strong signature algorithm (e.g., RS256 or HS256) and a library such as firebase/php-jwt for secure implementation.
| } catch (\PDOException $e) { | ||
| $this->pool['default'] = new \PDO($dsn, $user, $pass, $opts); | ||
| } |
There was a problem hiding this comment.
The catch (\PDOException $e) block attempts a fallback connection but completely swallows the original exception. If both connection attempts fail, only the second exception will be visible, and the root cause of the first failure will be lost. This can make debugging database connection issues very difficult. It's important to log the caught exception before attempting the fallback.
} catch (\PDOException $e) {
// TODO: Log the exception $e to avoid losing information about why the first connection attempt failed.
$this->pool['default'] = new \PDO($dsn, $user, $pass, $opts);
}| { | ||
| public function login(ServerRequestInterface $request) | ||
| { | ||
| return ['token' => 'DEV-TOKEN']; |
There was a problem hiding this comment.
The login method returns a hardcoded token string 'DEV-TOKEN'. While likely intended for development purposes, if this code is deployed to production, it provides a predictable and static credential that could be used to bypass authentication if the rest of the system accepts it. This should be replaced with a dynamic token generation process once the TokenService is secured.
composer.json
Outdated
| "team-reflex/discord-php": "^10.45.22" | ||
| "team-reflex/discord-php": "^10.45.22", | ||
| "monolog/monolog": "^3.0", | ||
| "sharkk/router": "*" |
There was a problem hiding this comment.
The dependency sharkk/router is set to version *. This is not recommended as it can pull in breaking changes from new major versions automatically, which could break your application unexpectedly. Since this package does not have tagged versions, it's better to lock it to a specific commit hash to ensure repeatable builds.
| "sharkk/router": "*" | |
| "sharkk/router": "dev-main#f46338a" |
| protected string $id; | ||
| protected string $status = self::STATUS_PENDING; | ||
| protected ?string $pauseReason = null; |
There was a problem hiding this comment.
The properties id, status, and pauseReason are declared as protected. Since they are only accessed and modified through methods within this final class, they can be declared as private to improve encapsulation.
private string $id;
private string $status = self::STATUS_PENDING;
private ?string $pauseReason = null;| * with this source code in the LICENSE.md file. | ||
| */ | ||
|
|
||
| namespace BackendPhp\Contract\Contract; |
There was a problem hiding this comment.
| } catch (\Throwable $e) { | ||
| // fallthrough | ||
| } |
There was a problem hiding this comment.
The catch (\Throwable $e) block is empty. If a date string is invalid and new \DateTimeImmutable($value) throws an exception, the error is silently ignored, and the code falls through to assign the original (invalid) string value to the attribute. This can hide data integrity issues. You should consider logging the exception to make it easier to track down invalid date formats.
} catch (\Throwable $e) {
// Consider logging this exception to track invalid date formats.
// fallthrough
}| // Eagerly bind our PartFactory to use the Discord factory so parts created | ||
| // through PartFactory will use the Discord client where appropriate. | ||
| $factory = $container->get('discord.factory'); | ||
| \BackendPhp\Support\PartFactory::setDiscordFactory($factory); |
There was a problem hiding this comment.
Using a static setter PartFactory::setDiscordFactory() introduces global state into the application. This can make the system harder to reason about and test, as the behavior of PartFactory::create() now depends on whether this static method has been called. A more robust approach would be to register PartFactory itself as a service in the container and inject it where needed, rather than relying on static methods.
There was a problem hiding this comment.
Pull request overview
This PR lays down an initial PHP backend foundation under the new BackendPhp\ namespace, including a minimal ReactPHP-based HTTP/WS server skeleton, basic domain utilities, a PDO migration runner/connection pool, and initial Pest-style tests.
Changes:
- Add foundational backend modules (Account/Auth/Character/Contract/Research/Reservation) with Handler/Service/Repository scaffolding.
- Introduce basic infrastructure (container, config, logging, parts factory integration with DiscordPHP, migrations runner, connection pool).
- Update Composer dependencies/autoloading and add initial automated tests + an integration migration fixture.
Reviewed changes
Copilot reviewed 51 out of 52 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| composer.json | Adds new dependencies and PSR-4 autoloading for BackendPhp\. |
| tests/integration_migrations/001_init.sql | Adds a minimal SQL migration fixture for integration testing. |
| tests/bootstrap.php | Adds test bootstrap and a Discord Part factory stub for tests. |
| tests/WorkUnitTest.php | Adds WorkUnit conversion tests (Pest-style). |
| tests/Reservation/ReservationTest.php | Adds Reservation service tests (Pest-style). |
| tests/Part/PartTest.php | Adds tests for Part behavior (nested parts + dirty tracking). |
| tests/IntegrationTest.php | Adds integration test for applying SQL migrations via SQLite memory DB. |
| tests/EffortTest.php | Adds tests for effort/efficiency calculations. |
| tests/Domain/ExpertiseTest.php | Adds tests for weighted expertise calculator. |
| tests/Contract/ContractLifecycleTest.php | Adds tests for contract lifecycle state transitions. |
| src/IA/backend/bin/server.php | Adds backend entrypoint to boot React loop and HTTP/WS skeleton servers. |
| src/IA/backend/World/Ticker.php | Adds a periodic timer “ticker” skeleton for game loop logic. |
| src/IA/backend/Utils/Helpers.php | Adds an env() helper wrapper. |
| src/IA/backend/Support/ServiceProvider.php | Adds base ServiceProvider abstraction. |
| src/IA/backend/Support/PartFactory.php | Adds PartFactory bridging local parts + DiscordPHP parts instantiation. |
| src/IA/backend/Support/MonologFactory.php | Adds a simple Monolog logger factory. |
| src/IA/backend/Support/DiscordServiceProvider.php | Adds DI registration for Discord client/factory and binds PartFactory. |
| src/IA/backend/Support/Container.php | Adds a minimal service container for lazy service resolution. |
| src/IA/backend/Support/Config.php | Adds a simple config wrapper and JSON config loader. |
| src/IA/backend/Server/WebSocket.php | Adds a placeholder WS server class. |
| src/IA/backend/Server/Http.php | Adds a React\Http server wrapper and router dispatch wiring. |
| src/IA/backend/Reservation/Service.php | Adds a simple inventory reservation service. |
| src/IA/backend/Research/TrialService.php | Adds a placeholder research trial service. |
| src/IA/backend/Research/Service.php | Adds async-style research service using React promises. |
| src/IA/backend/Research/Repository.php | Adds placeholder research repository. |
| src/IA/backend/Research/Handler.php | Adds HTTP handler for research trial endpoint. |
| src/IA/backend/Parts/Contract.php | Adds DiscordPHP-backed Contract Part with dirty tracking + serialization. |
| src/IA/backend/Parts/Character.php | Adds DiscordPHP-backed Character Part with dirty tracking + serialization. |
| src/IA/backend/Migrations/Runner.php | Adds a SQL migrations runner with a migrations tracking table. |
| src/IA/backend/Domain/WorkUnit.php | Adds WorkUnit utility for WU conversion. |
| src/IA/backend/Domain/ExpertiseCalculator.php | Adds weighted expertise calculation utility. |
| src/IA/backend/Domain/EffortCalculator.php | Adds effort/efficiency calculation utilities. |
| src/IA/backend/Database/Postgres.php | Adds a Postgres DSN helper. |
| src/IA/backend/Database/ConnectionPool.php | Adds a basic single-connection PDO “pool”. |
| src/IA/backend/Contract/Types.php | Adds contract type constants. |
| src/IA/backend/Contract/Service.php | Adds async-style contract service scaffold. |
| src/IA/backend/Contract/Resolver.php | Adds placeholder contract resolver. |
| src/IA/backend/Contract/Repository.php | Adds async-style contract repository scaffold. |
| src/IA/backend/Contract/PartRepository.php | Adds a repository to hydrate Contract parts from DB rows. |
| src/IA/backend/Contract/Handler.php | Adds HTTP handler for contract creation. |
| src/IA/backend/Contract/Contract.php | Adds contract domain model with basic status transitions. |
| src/IA/backend/Character/Service.php | Adds async-style character service scaffold. |
| src/IA/backend/Character/Repository.php | Adds async-style character repository scaffold. |
| src/IA/backend/Character/Handler.php | Adds HTTP handler for character stats endpoint. |
| src/IA/backend/Auth/TokenService.php | Adds token creation/verification utility. |
| src/IA/backend/Auth/Handler.php | Adds placeholder auth handler endpoints. |
| src/IA/backend/Auth/AuthMiddleware.php | Adds auth middleware that checks Bearer tokens. |
| src/IA/backend/Account/Service.php | Adds async-style account service scaffold. |
| src/IA/backend/Account/Repository.php | Adds async-style account repository scaffold. |
| src/IA/backend/Account/Handler.php | Adds HTTP handler for account endpoint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $pdo->exec('CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, ran_at TIMESTAMPTZ DEFAULT now())'); | ||
| } | ||
|
|
||
| $files = glob(rtrim($this->migrationsPath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'*.sql'); |
There was a problem hiding this comment.
Migrations are applied in the order returned by glob(), which is filesystem-dependent and not guaranteed to be sorted. For deterministic migration behavior, sort the list (e.g., by filename) before applying.
| $files = glob(rtrim($this->migrationsPath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'*.sql'); | |
| $files = glob(rtrim($this->migrationsPath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'*.sql'); | |
| if ($files === false) { | |
| $files = []; | |
| } | |
| sort($files, SORT_STRING); |
| public function jsonSerialize(): array | ||
| { | ||
| $out = []; | ||
| foreach ($this->getRawAttributes() as $k => $v) { | ||
| if ($v instanceof DiscordPart) { | ||
| $out[$k] = $v->jsonSerialize(); | ||
| } elseif (is_array($v)) { | ||
| $out[$k] = array_map(fn ($x) => $x instanceof DiscordPart ? $x->jsonSerialize() : $x, $v); | ||
| } else { | ||
| $out[$k] = $v; | ||
| } | ||
| } |
There was a problem hiding this comment.
The JSON serialization currently passes DateTimeImmutable values through as objects, which json_encode() will not serialize into an ISO string (often resulting in {} in JSON). Consider detecting \DateTimeInterface values in jsonSerialize() and converting them to a string format (e.g., ATOM) so API consumers get usable timestamps.
| // Try DSN-only construction first (works for sqlite and many drivers), | ||
| // fall back to DSN+credentials+options if that fails. | ||
| try { | ||
| $this->pool['default'] = new \PDO($dsn); | ||
| $this->pool['default']->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); | ||
| } catch (\PDOException $e) { | ||
| $this->pool['default'] = new \PDO($dsn, $user, $pass, $opts); | ||
| } |
There was a problem hiding this comment.
This DSN-only connection attempt can succeed while ignoring configured credentials/options (e.g., for drivers that allow OS-user auth), which makes behavior depend on environment and can be surprising in production. Prefer constructing PDO with DSN + credentials + options consistently (passing null is fine) so config is always honored.
| // Try DSN-only construction first (works for sqlite and many drivers), | |
| // fall back to DSN+credentials+options if that fails. | |
| try { | |
| $this->pool['default'] = new \PDO($dsn); | |
| $this->pool['default']->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); | |
| } catch (\PDOException $e) { | |
| $this->pool['default'] = new \PDO($dsn, $user, $pass, $opts); | |
| } | |
| $this->pool['default'] = new \PDO($dsn, $user, $pass, $opts); |
| test('1 minute (60s) equals 1 WU', function () { | ||
| $wu = WorkUnit::fromActiveSeconds(60); | ||
| Assert::assertEquals(1.0, $wu); | ||
| }); |
There was a problem hiding this comment.
These tests use Pest's test() function, but the repository's phpunit.xml is configured to run under PHPUnit directly. Without running via vendor/bin/pest (or configuring PHPUnit to load Pest), this file will fatal with Call to undefined function test(). Either switch to PHPUnit-style TestCase classes or ensure the test runner is Pest-based.
| * with this source code in the LICENSE.md file. | ||
| */ | ||
|
|
||
| namespace BackendPhp\Contract\Contract; |
There was a problem hiding this comment.
Namespace/path mismatch: this file is located at BackendPhp/Contract/PartRepository.php but declares namespace BackendPhp\Contract\Contract; which breaks PSR-4 autoloading (BackendPhp\\ => src/IA/backend/). Adjust the namespace (or move the file) so it can be autoloaded.
| namespace BackendPhp\Contract\Contract; | |
| namespace BackendPhp\Contract; |
| $this->attributes[$name] = $value; | ||
| } | ||
|
|
||
| public function __isset(string $name): bool | ||
| { | ||
| return isset($this->attributes[$name]); |
There was a problem hiding this comment.
Overriding __set() here writes directly to $this->attributes, which may bypass DiscordPHP Part's built-in attribute/mutator logic (e.g., setCreatedAtAttribute() / setUpdatedAtAttribute() won’t run on property assignment). Consider delegating to parent::__set() when possible.
| $this->attributes[$name] = $value; | |
| } | |
| public function __isset(string $name): bool | |
| { | |
| return isset($this->attributes[$name]); | |
| parent::__set($name, $value); | |
| } | |
| public function __isset(string $name): bool | |
| { | |
| return parent::__isset($name); |
|
|
||
| public function create(ServerRequestInterface $request): PromiseInterface | ||
| { | ||
| return $this->service->create(json_decode((string) $request->getBody(), true)); |
There was a problem hiding this comment.
json_decode(..., true) can return null for invalid JSON, but Service::create() is typed to accept an array. This will raise a TypeError instead of returning a 4xx response. Validate the JSON decode result (and/or decode with JSON_THROW_ON_ERROR) and convert invalid JSON into a 400-style error response.
| return $this->service->create(json_decode((string) $request->getBody(), true)); | |
| $body = (string) $request->getBody(); | |
| $data = json_decode($body, true); | |
| if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { | |
| return \React\Promise\reject( | |
| new \InvalidArgumentException('Invalid JSON body for create request.') | |
| ); | |
| } | |
| return $this->service->create($data); |
| // Return account object for authenticated user | ||
| $res = ['id' => $auth['sub'] ?? null, 'banked' => 0]; | ||
|
|
||
| return resolve($res); |
There was a problem hiding this comment.
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.
| // 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; | |
| } | |
| ); |
src/IA/backend/Auth/TokenService.php
Outdated
| class TokenService | ||
| { | ||
| public function createToken(array $payload): string | ||
| { | ||
| return base64_encode(json_encode($payload)); | ||
| } | ||
|
|
||
| public function verifyToken(string $token): ?array | ||
| { | ||
| return json_decode(base64_decode($token), true) ?: null; |
There was a problem hiding this comment.
TokenService creates tokens by base64-encoding JSON and verifyToken simply decodes them without any signature, MAC, or expiry checks, so there is no guarantee the token was issued or approved by the server. An attacker can forge arbitrary tokens (e.g., with any sub or role claims) by base64-encoding their own JSON and have them accepted as valid by code that trusts this payload for authentication/authorization. Replace this with a cryptographically signed token scheme (such as JWT with HMAC/RS* using a server-side secret or opaque random tokens stored server-side) and enforce signature and expiration checks before treating a token as valid.
| class TokenService | |
| { | |
| public function createToken(array $payload): string | |
| { | |
| return base64_encode(json_encode($payload)); | |
| } | |
| public function verifyToken(string $token): ?array | |
| { | |
| return json_decode(base64_decode($token), true) ?: null; | |
| use Firebase\JWT\JWT; | |
| use Firebase\JWT\Key; | |
| class TokenService | |
| { | |
| private const SECRET_KEY = 'change_this_secret_key'; | |
| private const ALGORITHM = 'HS256'; | |
| private const DEFAULT_TTL_SECONDS = 3600; | |
| public function createToken(array $payload): string | |
| { | |
| if (!isset($payload['exp'])) { | |
| $issuedAt = time(); | |
| $payload['iat'] = $payload['iat'] ?? $issuedAt; | |
| $payload['exp'] = $payload['exp'] ?? ($issuedAt + self::DEFAULT_TTL_SECONDS); | |
| } | |
| return JWT::encode($payload, self::SECRET_KEY, self::ALGORITHM); | |
| } | |
| public function verifyToken(string $token): ?array | |
| { | |
| try { | |
| $decoded = JWT::decode($token, new Key(self::SECRET_KEY, self::ALGORITHM)); | |
| $decodedArray = json_decode(json_encode($decoded), true); | |
| return is_array($decodedArray) ? $decodedArray : null; | |
| } catch (\Throwable $e) { | |
| return null; | |
| } |
| $payload = $this->tokens->verifyToken($token); | ||
| if ($payload) { | ||
| return $next($request->withAttribute('auth', $payload)); | ||
| } |
There was a problem hiding this comment.
AuthMiddleware::handle treats any non-null payload from TokenService::verifyToken as an authenticated identity and passes it downstream via the auth request attribute. Because TokenService accepts any base64-encoded JSON as a "valid" token, an attacker can craft arbitrary Bearer tokens to impersonate other users or gain unauthorized access wherever auth is used for authorization decisions. After securing token creation/verification with proper signatures and expiry, this middleware should reject tokens that fail verification and enforce required claims (e.g., issuer, audience, expiration) before calling $next.
| $payload = $this->tokens->verifyToken($token); | |
| if ($payload) { | |
| return $next($request->withAttribute('auth', $payload)); | |
| } | |
| try { | |
| $payload = $this->tokens->verifyToken($token); | |
| } catch (\Throwable $e) { | |
| $payload = null; | |
| } | |
| if ($payload !== null) { | |
| // Normalize payload to array for claim checks | |
| if (is_array($payload)) { | |
| $claims = $payload; | |
| } elseif ($payload instanceof \stdClass) { | |
| $claims = (array) $payload; | |
| } else { | |
| $claims = null; | |
| } | |
| // Require a valid, non-expired 'exp' claim before treating the token as authenticated | |
| if (is_array($claims) | |
| && array_key_exists('exp', $claims) | |
| && (is_int($claims['exp']) || ctype_digit((string) $claims['exp'])) | |
| && (int) $claims['exp'] >= time() | |
| ) { | |
| return $next($request->withAttribute('auth', $payload)); | |
| } | |
| } |
This pull request introduces a new backend architecture for the project, implementing foundational modules for account, authentication, character, contract, and database management. It also updates dependencies and autoloading in
composer.jsonto support the new backend structure. The most important changes are grouped below:Backend Module Foundations
Account,Auth,Character, andContract, each with their ownHandler,Service, andRepositoryclasses to establish clear separation of concerns and enable asynchronous operations using ReactPHP promises. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13]Contract.php), contract types (Types.php), and a resolver for contract completion (Resolver.php). [1] [2] [3]Authentication and Security
TokenService.php, and anAuthMiddlewarefor request authentication, returning 401 responses for unauthorized access. [1] [2]Database Layer
ConnectionPoolfor PDO connections and a helper for generating PostgreSQL DSNs. [1] [2]Composer and Autoloading Updates
composer.jsonto include new dependencies (monolog/monolog,sharkk/router) and autoloading for the new backend namespaceBackendPhpThis pull request introduces a new backend architecture for the project, implementing foundational modules for account, authentication, character, contract, and database management. It also updates dependencies and autoloading incomposer.json` to support the new backend structure. The most important changes are grouped below:Backend Module Foundations
Account,Auth,Character, andContract, each with their ownHandler,Service, andRepositoryclasses to establish clear separation of concerns and enable asynchronous operations using ReactPHP promises. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13]Contract.php), contract types (Types.php), and a resolver for contract completion (Resolver.php). [1] [2] [3]Authentication and Security
TokenService.php, and anAuthMiddlewarefor request authentication, returning 401 responses for unauthorized access. [1] [2]Database Layer
ConnectionPoolfor PDO connections and a helper for generating PostgreSQL DSNs. [1] [2]Composer and Autoloading Updates
. [1] [2]
Frontend Architecture Documentation
WebSocketManagerclass fromprivatetoprotectedin the frontend architecture design doc to allow for easier extension. [1] [2]