Fix the phpseclib conflict: own the Solid-OIDC client, verify the callback, and adopt release/v* tagging - #9
Merged
Merged
Conversation
- Fix hardcoded 'solid:3000' references in PodService.php comments - Add null safety checks for parse_url() results across multiple methods - Fix Utils::getSolidServerUrl() to construct URL from config components - Add JSON decode error handling in OpenIDConnectClient.php - Improve error messages for invalid WebID formats This commit addresses the following issues: 1. Critical: Misleading hardcoded example in getPodUrlFromWebId() comments 2. High: Missing null/error checks on parse_url() results 3. High: Configuration inconsistency in getSolidServerUrl() 4. High: Unhandled JSON decode failures in retrieve() and loadDPoPKeyPair() All changes maintain backward compatibility while improving error handling and code reliability.
Solid could not be installed alongside current Core API: jumbojett/openid-connect-php pins phpseclib/phpseclib ^3.0.7 and laravel/socialite ^5.31 requires ^4.0, so Composer refused to resolve the application. Upstream Jumbojett has not migrated to phpseclib 4 and phpseclib 4 renamed its root namespace to phpseclib4\, so widening the constraint would resolve and then fatal at runtime. The pin bought nothing. Jumbojett touches phpseclib only in verifyRSAJWTSignature(), reachable only from verifyJWTSignature() / verifySignatures() -- and this extension overrode the former while the callback path (OIDCController -> exchangeCodeForTokens) never reached Jumbojett's authenticate() at all. Transport, token requests, client registration, session storage and discovery were all overridden too, so about 150 lines of a 2,100-line dependency were in use. The corollary is the important part: the callback had no ID token signature verification, no state check and no nonce check, and the WebID driving all pod access was read from an unverified JWT. So the library is removed rather than replaced. Socialite is not the alternative -- Solid-OIDC needs RFC 7591 dynamic client registration, RFC 9449 DPoP-bound tokens and a per-tenant issuer discovered at runtime, none of which Socialite provides -- but Core API's *verification* conventions are reused: SolidIdTokenVerifier mirrors Fleetbase\Auth\OAuth\IdTokenVerifier (300s JWKS cache, resolve by kid, one forced refetch on an unknown kid, TLS never inferred from the environment). It uses firebase/php-jwt rather than lcobucci/jwt because Core's verifier hardcodes RS256 and a Solid provider may sign with PS256 or ES256. Security, all previously absent on the callback: - ID token signature, iss, aud, azp, expiry, nonce and sub are verified - state is validated server side, single use, consumed before comparison - alg confusion is blocked: a key without alg is dropped rather than assumed RS256, so alg:none and HS256-with-the-RSA-public-key both fail - PKCE S256 is mandatory instead of silently dropped - DPoP private keys move off the application default disk, which is the web-served `public` disk -- an existing key is migrated and the exposed copy deleted, so nobody is signed out - OIDC requests no longer follow redirects, which could replay an Authorization: Basic client secret to another host - access and DPoP tokens are no longer written to the logs - state/nonce/code_verifier now carry a TTL; they had none - the access token's cnf.jkt is checked against our DPoP key thumbprint Also fixed: DPoP htu strips query and fragment per RFC 9449 4.2; the RFC 9449 8 use_dpop_nonce retry is honoured; discovery is cached instead of refetched on every Solid request; provider errors on the callback are surfaced; authenticate() returns a redirect rather than header()+exit; registration declares the refresh_token grant so the requested offline_access scope is usable; scopes are deduplicated. Dependencies: removed jumbojett/openid-connect-php, all seven web-token/jwt-*, php-http/guzzle7-adapter and psr/http-factory-implementation (none referenced anywhere); added firebase/php-jwt, already in use but undeclared; php ^8.0 -> ^8.1. Tests: 1 placeholder -> 108 tests / 208 assertions, verified on PHP 8.2 and 8.4. test:unit runs phpunit: pest's binary resolves its autoloader from a hardcoded vendor/, which this package does not have, so it could never start here -- which is why the suite was disabled in CI. BREAKING: exchangeCodeForTokens() now requires the callback state, and Jumbojett\OpenIDConnectClientException is replaced by Fleetbase\Solid\Exceptions\OpenIDConnectClientException.
Brings this repository onto the same release path as fleetops and storefront, which it was missing entirely: merging a release branch to main produced no tag, so nothing published. - release.yml (new) delegates to fleetbase/fleetbase/.github/workflows/release-tag.yml@main, gated on a merged PR whose head branch is release/v* (or the legacy dev-v*). It pushes the tag and nothing else; create-release.yml and the publish jobs in ember.yml already chain off `push: tags: v*`. version-files is composer.json,package.json,extension.json -- this repository is an ember addon plus a server, so it carries all three, and the tag is refused unless they and RELEASE.md all name the version in the branch. - RELEASE.md (new) is required by that workflow, and its first line must name the version being released -- that check is what distinguishes notes written for this release from the previous release's leftovers. - create-release.yml publishes RELEASE.md as the release body, matching fleetops and storefront. - ember.yml and server.yml also run on pull requests into a release branch, so work staged on release/v* is checked before the release PR is opened. - server.yml gains the Composer GitHub auth step both reference repos have, and runs the test suite again now that it runs under phpunit. Coverage jobs are deliberately not copied across: this package has no coverage:baseline script or scripts/coverage-summary.php to gate on.
The Ember job fails at `pnpm install` with ERR_PNPM_IGNORED_BUILDS:
Ignored build scripts: @fortawesome/fontawesome-common-types@6.4.0,
@fortawesome/fontawesome-svg-core@6.4.0,
@fortawesome/free-brands-svg-icons@6.4.0,
@fortawesome/free-solid-svg-icons@6.4.0, core-js@2.6.12
pnpm 10 refuses to install when a dependency has a build script that has
not been explicitly allowed or denied, and the workflow installs
`version: latest`. fleetops and storefront answer that with a
pnpm-workspace.yaml denying builds for exactly these packages -- none of
them needs its postinstall to produce a working addon build -- which is
why their Ember CI passes and this repository's has not since pnpm 10
shipped. Copied from fleetops verbatim.
Unrelated to the OIDC work on this branch; it is environmental drift that
would fail the same way on main today.
Fix critical and high-priority bugs
- NODE_VERSION: 22.x at the workflow level, read by all four jobs, matching fleetops and storefront. The build job's single-entry matrix is gone, so there is one place to change the version instead of five. - @fleetbase/ember-core ^0.3.10 -> ^0.3.24 @fleetbase/ember-ui ^0.3.17 -> ^0.4.3 @fleetbase/fleetops-data ^0.1.25 -> ^0.2.2 All three at the latest published versions. ember-ui crosses a 0.3 -> 0.4 boundary, so ^0.3.17 would never have taken it. - fleetbase_publish_qa removed. Verified: `pnpm install` clean (peer warnings are the pre-existing ember-source 5.4.1 vs older peer ranges, same as fleetops) and `pnpm run build` -> "Built project successfully" on node 22. Solid imports only three symbols from these packages -- ember-core's contracts, exports and utils/get-with-default -- and all three still resolve.
1. PodService resolved a class that does not exist. Commit 907664b ("remove CSS credential code - use OIDC tokens only") deleted CssAccountService and CssAccountController but left two `app(CssAccountService::class)` call sites behind in PodService. There is no `use` for it, so the name resolved to Fleetbase\Solid\Services\CssAccountService and the container threw on entry -- taking out pod creation and the pod listing. Both are "try the CSS account API, fall through to the real method" wrappers, so this finishes what 907664b started and removes them; the working paths underneath are untouched. The css_email/css_password columns are now referenced nowhere, but the migration that adds them is left in place -- dropping columns holding encrypted credentials is a separate, destructive decision. 2. getAccountIndex() ended in dd(). On the live, fleetbase.protected GET solid/int/v1/account route, so it halted the request and dumped the raw pod response to the browser. It returns JSON now, handling the unauthenticated and unsuccessful cases the way getAuthenticationStatus() does. 3. SolidClient ignored the saved server configuration. saveServerConfig() writes system.solid.server and getServerConfig() reads it back, but the client only ever read config('solid.server.*') -- so changing the host or port in the console changed nothing. Resolution is now explicit option -> saved setting -> config default. Two related bugs in the same three lines: the old code passed `data_get($options, 'host')` as config()'s *default*, and since the key is always defined a caller-supplied host was silently discarded; and `public SolidIdentity $identity` was typed non-nullable while being assigned from an absent option, so `new SolidClient([])` raised a TypeError. The property is nullable now. saveServerConfig() also flushes the OIDC provider memo, since the discovery base derives from the server URL and a long-lived worker would otherwise keep talking to the old provider. While auditing the route table for the duplicate `account` registration, five more routes turned out to point at controller methods that do not exist: SolidController@play, @getProfileData, @getSyncStatus, @syncVehicles, @syncDrivers, @syncOrders and @syncall. Nothing in the console calls any of them and each would 500 on hit, so they are removed. The sync services themselves stay and remain reachable through DataController@importResources; the profile payload already reaches the console through authentication-status. Every routed method now resolves to a defined one.
`composer test` has been failing in all three of its stages. It passes now.
test:unit -- back on Pest, via storefront's scripts/pest-runner.php.
The earlier commit on this branch switched to phpunit because Pest's
binary resolves its autoloader from a hardcoded `vendor/`, which this
package does not have (vendor-dir is server_vendor), so `pest` could not
start at all. That was a workaround; storefront already solved it
properly with a runner that symlinks vendor -> server_vendor for the
duration of the run and removes it afterwards. Adopted verbatim, so this
package is back on the house test runner.
scripts/pest-bootstrap.php is deliberately near-empty and in particular
does NOT require the autoloader. Storefront's does, because its shims
reference Illuminate classes at prepend time -- but loading the
autoloader before Pest boots leaves its reporter with nothing to print:
the suite still runs and still exits 0, and the report silently vanishes.
That cost an hour; the comment in the file says so.
test:types -- phpstan is clean at level max against a baseline.
275 errors, essentially all Laravel symbols phpstan cannot discover
(facade __callStatic, Eloquent magic properties, global helpers) plus
untyped legacy signatures. larastan would remove most of them but is not
usable here: it needs laravel/framework, and this package requires only
the illuminate/* components it uses, so it dies on an undefined
LARAVEL_VERSION constant.
So: phpstan-baseline.neon, generated and committed. New code is held to
level max -- nothing written on this branch is in the baseline -- and the
debt is explicit and reviewable rather than a permanently red command.
Also `--memory-limit=0`, which this phpstan version rejects outright
("Memory limit \"0\" cannot be set"), is now -1, as core-api has it.
test:lint -- the six long-unformatted files are formatted.
PodService, AclService, ResourceSyncService, DataController,
Support/Utils and routes.php. Mechanical php-cs-fixer output, no
behaviour change; tests pass unchanged either side of it.
CI now enforces all of it:
- Run Lint uses test:lint, not lint. `composer lint` runs php-cs-fixer in
*fix* mode, so in CI it rewrote files in the runner and exited 0
whatever the state of the branch -- it was never a gate.
- Run Static Analysis is new, and enforceable for the first time.
- Coverage is generated, uploaded as an artifact and sent to Codecov,
following storefront. No --fail-under gate, unlike fleetops: most of
this package predates its tests and sits at 0%, so a gate would only
ever be red.
Coverage today is 25.45% overall, and the code added on this branch is the
covered part: SolidIdTokenVerifier 95.7%, DPoPKeyPair 93.2%,
OpenIDConnectClient 81.4%, CachedJwksResolver and RedisStateStore 100%.
Codecov badge added to the README, alongside repo/license/npm/packagist/
node/php badges in the same shape fleetops uses.
RELEASE.md was written before the four pre-existing defects and the engine dependency upgrades landed on this branch.
Both sides touched PodService and Utils. PR #8 added parse_url() guards in six places. Two of them were inside the CSS-credential branches this branch deletes -- the service they call was removed in 907664b and never existed at runtime -- so those guards go with the code. The two in getPodUrlFromWebId() and getStorageUrlFromWebId() are kept, and getUserPods() is still covered because it now reaches getStorageUrlFromWebId() rather than parsing the WebID itself. #8's corrected example comments are kept. Utils::getSolidServerUrl() needed more than a textual resolution. #8 fixed it to build the URL from config('solid.server.*') instead of a non-existent solid.server.url; this branch separately taught SolidClient to prefer the host and port an administrator saved through the console. Left as merged, the two would report different servers -- the same class of inconsistency #8 set out to fix. Utils now delegates to SolidClient::serverUrl(), and both it and the instance getServerUrl() go through one private builder, so there is a single answer. phpstan flagged that builder being reached through static:: while private; it is self:: now, and the baseline is regenerated (242 errors, down from 247, since #8's guards replaced some untyped access).
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. Thanks for integrating Codecov - We've got you covered ☂️ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Fleetbase could not resolve its dependencies with this extension installed:
Upstream Jumbojett has not migrated to phpseclib 4 —
masterstill declares^3.0.7and still importsphpseclib3\Crypt\RSA. phpseclib 4 renamed its root namespace tophpseclib4\, so widening the constraint to^3.0.7 || ^4.0resolves and then fatals at runtime. Pinning Core API or Socialite back was not on the table.The audit found the pin was bought for nothing. Jumbojett touches phpseclib in exactly one method,
verifyRSAJWTSignature(), reachable only fromverifyJWTSignature()/verifySignatures()— and this extension overrode the former, while the callback path (OIDCController@completeRegistration→exchangeCodeForTokens) never reached Jumbojett'sauthenticate()at all. Transport, token requests, client registration, session storage and discovery were all overridden too. About 150 lines of a 2,100-line dependency were in use, and phpseclib never executed on any Solid code path.The corollary is the part that matters: the authorization callback had no ID token signature verification, no
statecheck and nononcecheck, and the WebID that drives every pod read and write was taken from an unverified JWT.What
Jumbojett is removed and the Solid-OIDC client lives here, in
server/src/Client/OpenIDConnectClient.phpandserver/src/Auth/.Socialite is not the alternative, and this was checked before deciding: Solid-OIDC needs RFC 7591 dynamic client registration, RFC 9449 DPoP-bound tokens and an issuer discovered per tenant at runtime, and
Laravel\Socialite\Two\AbstractProviderprovides none of the three. Core API's OAuth drivers sign users into Fleetbase from fixed IdPs; Solid makes Fleetbase a client of a user's pod.What is reused is Core API's verification half.
SolidIdTokenVerifierdeliberately mirrorsFleetbase\Auth\OAuth\IdTokenVerifier— same 300s JWKS cache constant, resolve bykid, one forced refetch on an unknownkid, claims returned rather than a boolean, TLS never inferred from the app environment. It diverges in two documented places: it usesfirebase/php-jwtrather thanlcobucci/jwt, because Core's verifier hardcodesLcobucci\JWT\Signer\Rsa\Sha256(RS256 only) and a Solid provider is operator-chosen with node-oidc-provider also offering PS256 and ES256; and it validatesnonce, which Core has no reason to know about.Every collaborator is injectable, which is what makes the handshake testable without a network or Redis.
Security
All of these were absent on the callback before:
iss(exact),aud,azpwhenaudis multi-valued (OIDC Core 3.1.3.7), expiry with a bounded 60s leeway,nonce, andsubpresence.statevalidation — server-side, single use, consumed before comparison so a forged callback cannot be retried; a mismatch voids the whole pending request.algis dropped rather than assumed RS256, and php-jwt rejects a header algorithm differing from the key's, soalg: noneand HS256-signed-with-the-RSA-public-key both fail. Both are tested.One vulnerability found in the code being rewritten, and fixed:
Also: PKCE S256 is mandatory instead of silently dropped; TLS verification is one explicit flag rather than inferred from
APP_ENV(which also disabled it in any environment namedlocal/development); OIDC requests no longer follow redirects, which could replay anAuthorization: Basicclient secret to another host; access and DPoP tokens are no longer written to the logs;state/nonce/code_verifiernow carry a TTL where they previously had none; and the access token'scnf.jktis checked against our DPoP key thumbprint.Not changed, needs a data migration:
solid_identities.token_responsestores access and refresh tokens in plaintext viaFleetbase\Casts\Json.Other fixes
htustrips query and fragment per RFC 9449 §4.2 — a proof built for a URL with parameters was unusable against a server comparinghtustrictly.use_dpop_nonceis retried once..well-known/openid-configuration, so one controller action touching four resources made four extra round trips.erroron the callback is surfaced instead of becoming "missing authorization code".authenticate()returns aRedirectResponserather thanheader()+exit.grant_typesincludingrefresh_token; without it a provider defaults toauthorization_codealone, which made the requestedoffline_accessscope unusable. Existing registrations untouched.openid webid offline_access openid.Dependencies
Removed
jumbojett/openid-connect-php, all sevenweb-token/jwt-*,php-http/guzzle7-adapterandpsr/http-factory-implementation— theweb-tokenstack and the PSR adapters it needed had zero references anywhere in the package. Addedfirebase/php-jwt ^6.10|^7.0, which was already in use but relied on arriving transitively, plusext-json/ext-openssl.phpraised^8.0→^8.1.easyrdf/easyrdfandml/json-ldare also unused today but are RDF domain libraries unrelated to this conflict, so I left them — flagging for a call.CI
Brings this repository onto the same release path as fleetops and storefront, which it was missing entirely — merging a release branch to
mainproduced no tag, so nothing published.release.yml(new) delegates tofleetbase/fleetbase/.github/workflows/release-tag.yml@main, gated on a merged PR whose head branch isrelease/v*(or the legacydev-v*). It pushes the tag and nothing else;create-release.ymland the publish jobs inember.ymlalready chain offpush: tags: v*.version-filesiscomposer.json,package.json,extension.json— this repo is an ember addon plus a server, so it carries all three.RELEASE.md(new) is required by that workflow, and its first line must name the version being released.create-release.ymlpublishesRELEASE.mdas the release body, matching both reference repos.ember.ymlandserver.ymlalso run on PRs into a release branch, so work staged onrelease/v*is checked before the release PR is opened.server.ymlgains the Composer GitHub auth step both reference repos have, and runs the test suite again.On that last point:
composer test:unitnow runsphpunit. Pest's binary resolves its autoloader from a hardcodedvendor/, which this package does not have — it setsvendor-dirtoserver_vendor— sopestcould never start here. That is why the suite was commented out in CI and why the package had only a placeholder test.Coverage jobs are deliberately not copied across: this package has no
coverage:baselinescript orscripts/coverage-summary.phpto gate on.Since this branch is
release/v0.0.8and all three manifests plusRELEASE.mdagree on0.0.8, merging this will tag and release v0.0.8.Verification
core-api ^1.6.63+solid-api ^0.0.8, path repos, root'sphp >=8.0 <8.3)composer why --locked phpseclib/phpsecliblaravel/socialite v5.31.0 requires ^4.0composer why-not --locked phpseclib/phpseclib ^4.0phpuniton PHP 8.2.28 and 8.4.0php-cs-fixer --dry-runphpstan level maxmain; the OIDC client went 129 → 10, and all remaining errors in new code are Laravel symbol-discovery noise (no larastan)pest --filter AuthThe suite was run under PHP 8.2 specifically because that is the floor the app's combined graph resolves to (
lcobucci/jwt,cuyz/valinorneed~8.2.0), inside the root's>=8.0 <8.3.Dependency tree after
Tests
1 placeholder → 108 tests / 208 assertions.
SolidIdTokenVerifierTest(25) covers valid RS256 and ES256 plus every rejection path;OpenIDConnectClientTest(49) drives the whole handshake with the network scripted; plusDPoPKeyPairTest(14, RFC 7638 thumbprint and RFC 9449 proof shape),DPoPKeyStorageTest(5),CachedJwksResolverTest(8),RedisStateStoreTest(7).I found three bugs in my own code during review and reintroduced each one to confirm the new tests fail, then restored the fixes: a
SETEX ... 1that would have wiped every saved client registration a second after writing it, a missing-nonce path that silently skipped the nonce check, and the DPoP key disk.Backwards compatibility
No user has to re-authenticate. Existing Redis client registrations are reused (key and
CLIENT_NAMEunchanged) and existing DPoP keys are migrated rather than regenerated. The browser flow is unchanged —addon/controllers/home.jsdoes a full navigation, and a 302 is followed identically to the oldheader()+exit.To call out:
exchangeCodeForTokens($code, $state)now requires$state. The signature is unchanged for source compatibility, but a null/empty state throws — without it there is no CSRF control on the callback.Jumbojett\OpenIDConnectClientException→Fleetbase\Solid\Exceptions\OpenIDConnectClientException(same name, same\Exceptionparent). Nothing in-tree caught the old one.getCodeVerifier()removed — unused in-tree, andprotectedin Jumbojett, so no external caller could have had it.Needed outside this repo
Root
api/composer.json:"fleetbase/solid-api": "^0.0.8". A^0.0.7constraint will not pick this up, since Composer treats^0.0.xpatches as potentially breaking. No Core API changes — it already ships everything this aligned with, and its auth tests pass untouched.If
FILESYSTEM_DRIVERpoints at S3 or GCS, check that bucket for existingsolid/dpop_keys_*.jsonobjects and delete them; the automatic migration only covers the disk the app is currently configured with.Pre-existing issues found, not fixed here
PodServiceresolvesCssAccountService(lines 121, 350) — that class does not exist anywhere in the repository, so the CSS-account pod-creation path throws on entry.SolidController@getAccountIndex()ends indd()on the live protectedGET solid/int/v1/accountroute, which is also registered twice inroutes.php.SolidClientignores theSetting::system('solid.server')values the admin server-config UI writes, so that UI has no effect. Itsdata_get($options, ...)fallbacks are also dead, andnew SolidClient([])throws aTypeError.composer test:lintandtest:typeswere already failing before this branch: six long-unformatted files and 428 phpstan errors atlevel: max(no larastan). I formatted only what I touched and left the rest, so the counts move in the right direction without an unrelated reformatting diff.Follow-up work on this branch
Node 22 and the engine dependencies
NODE_VERSION: 22.xis now declared once at the workflow level and read by every job, matching fleetops and storefront. The build job's single-entrymatrixis gone, so there is one place to change the version instead of five.fleetbase_publish_qais removed.Engine dependencies moved to their latest published versions:
@fleetbase/ember-core^0.3.10^0.3.24@fleetbase/ember-ui^0.3.17^0.4.3@fleetbase/fleetops-data^0.1.25^0.2.2ember-uicrosses a 0.3 → 0.4 boundary, so^0.3.17would never have taken it. Verified withpnpm install(clean; the peer warnings are the pre-existingember-source5.4.1 vs older peer ranges, identical to fleetops) andpnpm run build→ "Built project successfully" on Node 22. Solid imports only three symbols from these packages — ember-core'scontracts,exportsandutils/get-with-default— and all three still resolve.Also added
pnpm-workspace.yaml, copied from fleetops. The Ember job was failing atpnpm installwithERR_PNPM_IGNORED_BUILDS— pnpm 10 refuses to install when a dependency has an unapproved build script, and the workflow installsversion: latest. fleetops and storefront both carry a file denying builds for exactly the packages this tripped on (@fortawesome/*,core-js,fsevices); solid was simply missing it, which is why its last green Ember run was 2025-12-30. Environmental drift, not this branch — it would fail the same way onmaintoday.The four pre-existing issues
All four are fixed here rather than deferred.
1.
PodServiceresolved a class that does not exist. Commit907664b("remove CSS credential code — use OIDC tokens only") deletedCssAccountServiceandCssAccountControllerbut left twoapp(CssAccountService::class)call sites behind. With nousestatement the name resolved toFleetbase\Solid\Services\CssAccountService, and the container threw on entry — taking out pod creation and the pod listing. Both were "try the CSS account API, fall through to the real method" wrappers, so this finishes what907664bstarted and removes them; the working paths underneath are untouched. Thecss_email/css_passwordcolumns are now referenced nowhere, but the migration that adds them is deliberately left in place — dropping columns holding encrypted credentials is a separate, destructive decision.2.
getAccountIndex()ended indd(). On the live,fleetbase.protectedGET solid/int/v1/accountroute. It returns JSON now, handling the unauthenticated and unsuccessful cases the waygetAuthenticationStatus()does, and the duplicate route registration is gone.While auditing the route table for that duplicate, seven more routes turned out to point at controller methods that do not exist:
SolidController@play,@getProfileData,@getSyncStatus,@syncVehicles,@syncDrivers,@syncOrdersand@syncAll. Each would 500 on hit and nothing in the console calls any of them, so they are removed — the sync services stay and remain reachable throughDataController@importResources, and the profile payload already reaches the console throughauthentication-status. Every routed method now resolves to a defined one, and that is asserted.3. The admin server-configuration UI had no effect.
saveServerConfig()wrotesystem.solid.serverandgetServerConfig()read it back, butSolidClientonly ever readconfig('solid.server.*')— so changing the Solid host or port in the console changed nothing. Resolution is now explicit option → saved setting → config default. Two more bugs lived in the same three lines: the old code passeddata_get($options, 'host')asconfig()'s default, and since the key is always defined a caller-supplied host was silently discarded; andpublic SolidIdentity $identitywas typed non-nullable while being assigned from an absent option, sonew SolidClient([])raised aTypeError.saveServerConfig()now also flushes the OIDC provider memo, since the discovery base derives from the server URL and a long-lived worker would otherwise keep talking to the old provider.4.
composer testfailed in all three stages. It passes now.test:unitis back on Pest, via storefront'sscripts/pest-runner.php. My earlier commit on this branch switched to phpunit because Pest's binary resolves its autoloader from a hardcodedvendor/and this package installs toserver_vendor. That was a workaround — storefront had already solved it properly with a runner that symlinksvendor→server_vendorfor the duration of the run and removes it after. Adopted verbatim, so this package is back on the house runner.scripts/pest-bootstrap.phpis deliberately near-empty and in particular does not require the autoloader: storefront's does, because its shims reference Illuminate classes at prepend time, but loading the autoloader before Pest boots leaves its reporter with nothing to print — the suite still runs and still exits 0, and the report silently vanishes. The comment in the file says so.test:typesis clean at phpstanlevel: maxagainst a committedphpstan-baseline.neon. The 275 errors were essentially all Laravel symbols phpstan cannot discover without larastan (facade__callStatic, Eloquent magic properties, global helpers) plus untyped legacy signatures. larastan is not usable here — it needslaravel/framework, and this package requires only theilluminate/*components it uses, so it dies on an undefinedLARAVEL_VERSIONconstant. I tried it and backed it out. The baseline holds new code to level max (nothing written on this branch is in it) and makes the debt explicit rather than leaving a permanently red command.--memory-limit=0, which this phpstan version rejects outright, is now-1as core-api has it.test:lint— the six long-unformatted files are formatted. Mechanical php-cs-fixer output, no behaviour change, tests pass unchanged either side.CI now enforces all of it.
Run Lintusestest:lintrather thanlint:composer lintruns php-cs-fixer in fix mode, so in CI it rewrote files in the runner and exited 0 whatever the state of the branch — it was never a gate.Run Static Analysisis new and enforceable for the first time.Codecov
Coverage is generated, uploaded as an artifact and sent to Codecov, following storefront. No
--fail-undergate, unlike fleetops: most of this package predates its tests and sits at 0%, so a gate would only ever be red. The report is published so the number is visible and can be moved deliberately.Today it reads 25.45% overall, and the code added on this branch is the covered part:
SolidIdTokenVerifierDPoPKeyPairOpenIDConnectClientCachedJwksResolver,RedisStateStoreThe codecov badge is in the README alongside repo / license / npm / packagist / node / php badges, in the shape fleetops uses.
Merge with PR #8
#8 was merged into this branch mid-flight and both sides touched
PodServiceandUtils.#8 added
parse_url()guards in six places. Two were inside the CSS-credential branches this branch deletes, so those guards went with the code — the service they call was removed in907664band never existed at runtime. The two ingetPodUrlFromWebId()andgetStorageUrlFromWebId()are kept, andgetUserPods()is still covered because it now reachesgetStorageUrlFromWebId()rather than parsing the WebID itself. #8's corrected example comments are kept.Utils::getSolidServerUrl()needed more than a textual resolution. #8 fixed it to build the URL fromconfig('solid.server.*')instead of a non-existentsolid.server.url; this branch separately taughtSolidClientto prefer the host and port an administrator saved through the console. Left as merged, the two would report different servers — the same class of inconsistency #8 set out to fix.Utilsnow delegates toSolidClient::serverUrl(), and both it and the instancegetServerUrl()go through one private builder.Verification after all of the above
composer test(lint + types + unit)phpuniton PHP 8.2.28 and 8.4.0pnpm run buildon Node 22OK (108 tests, 208 assertions)on PHP 8.2.33pnpm-workspace.yamlfixCssAccountService/css_emailreferencesOne thing I did not change:
SolidClient::request()still disables TLS verification for pod data requests based onapp()->environment('local','development'). That is the data transport rather than the OIDC handshake, and tightening it is a behaviour change for local development that belongs in its own PR.