From d2406151f57bf718d55c55e4165ff0b3e4dcfc3a Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:40:02 +0000 Subject: [PATCH 1/5] Raise Laravel security floors to 12.72 and 13.41 --- .github/workflows/ci.yml | 20 +++++----- CHANGELOG.md | 7 ++++ composer.json | 4 +- docs/laravel-adoption-contract.json | 12 +++--- docs/portal/frameworks/laravel.md | 15 +++++--- docs/portal/operate/upgrading.md | 6 +-- docs/sdk-reference.md | 16 ++++---- examples/laravel-http/composer.json | 2 +- examples/laravel-http/composer.lock | 16 ++++---- src/Bridge/Laravel/LaravelPlatform.php | 6 +-- tests/FrameworkBridgeTest.php | 2 +- tests/LaravelAdoptionContractTest.php | 28 +++++++------- tests/LaravelPlatformTest.php | 37 ++++++++++++------- tests/LaravelTransitionAuthorityTest.php | 28 +++++++++++--- tests/StaticCredentialSubjectTest.php | 4 +- .../dependency-intent.json | 4 +- 16 files changed, 123 insertions(+), 84 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f02f532..d1b52318 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -333,8 +333,8 @@ jobs: - framework: Laravel version: '12' php: '8.2' - package: 'laravel/framework:^12.69.0' - exercised_package: 'laravel/framework:12.69.0' + package: 'laravel/framework:^12.72.0' + exercised_package: 'laravel/framework:12.72.0' bootstrap: fresh_supported_install script: laravel.php - framework: Symfony @@ -656,15 +656,15 @@ jobs: - framework: Laravel version: '12' php: '8.4' - package: 'laravel/framework:^12.69.0' - exercised_package: 'laravel/framework:12.69.0' + package: 'laravel/framework:^12.72.0' + exercised_package: 'laravel/framework:12.72.0' bootstrap: fresh_supported_install script: laravel.php - framework: Laravel version: '13' php: '8.4' - package: 'laravel/framework:^13.30.0' - exercised_package: 'laravel/framework:13.30.0' + package: 'laravel/framework:^13.41.0' + exercised_package: 'laravel/framework:13.41.0' bootstrap: fresh_supported_install script: laravel.php - framework: Symfony @@ -828,15 +828,15 @@ jobs: - framework: Laravel version: '12' php: '8.3' - package: 'laravel/framework:^12.69.0' - exercised_package: 'laravel/framework:12.69.0' + package: 'laravel/framework:^12.72.0' + exercised_package: 'laravel/framework:12.72.0' bootstrap: fresh_supported_install script: laravel.php - framework: Laravel version: '13' php: '8.3' - package: 'laravel/framework:^13.30.0' - exercised_package: 'laravel/framework:13.30.0' + package: 'laravel/framework:^13.41.0' + exercised_package: 'laravel/framework:13.41.0' bootstrap: fresh_supported_install script: laravel.php steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 19bb239d..90f74eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Security + +- Raise the optional Laravel bridge floors to 12.72.0 and 13.41.0 for + the November concurrency and signed-URL advisories, reject affected installations before provider bindings, + and update the current qualification matrix and HTTP example dependency + graph. Historical qualification reports retain their original tuples (#421). + ## [3.18.2] - 2026-11-13 ### Fixed diff --git a/composer.json b/composer.json index 52cb788f..91ee9ac0 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ }, "suggest": { "ext-pcntl": "Required by framework console workers for graceful SIGINT and SIGTERM handling.", - "laravel/framework": "Enables the auto-discovered service-mode bridge on Laravel 12.69.0 or 13.30.0 and later releases of each major (the patched minimums for GHSA-f9x3-7vq9-cph6).", + "laravel/framework": "Enables the auto-discovered service-mode bridge on Laravel 12.72.0 or 13.41.0 and later releases of each major (the patched November 2026 security minimums).", "open-telemetry/api": "Optional worker tracing with the application's provider; supports API 1.10.1 and later in the 1.x line.", "symfony/framework-bundle": "Enables the Symfony 6.4, 7, and 8 service-mode bundle.", "symfony/console": "Required with symfony/framework-bundle to run the Symfony 6.4, 7, and 8 worker command." @@ -122,7 +122,7 @@ "external-payload-references": "opaque-reference" }, "frameworks": { - "laravel": "^12.69.0|^13.30.0", + "laravel": "^12.72.0|^13.41.0", "symfony": "^6.4|^7.0|^8.0" } } diff --git a/docs/laravel-adoption-contract.json b/docs/laravel-adoption-contract.json index 9674692b..e3d1b4e4 100644 --- a/docs/laravel-adoption-contract.json +++ b/docs/laravel-adoption-contract.json @@ -3,14 +3,14 @@ "schema_version": 1, "framework": { "package": "laravel/framework", - "constraint": "^12.69.0|^13.30.0", + "constraint": "^12.72.0|^13.41.0", "php_constraint": "^8.2", "qualification_matrix": [ - {"laravel": "12", "php": "8.2", "package": "laravel/framework:^12.69.0", "exercised_package": "laravel/framework:12.69.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "12", "php": "8.3", "package": "laravel/framework:^12.69.0", "exercised_package": "laravel/framework:12.69.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "13", "php": "8.3", "package": "laravel/framework:^13.30.0", "exercised_package": "laravel/framework:13.30.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "12", "php": "8.4", "package": "laravel/framework:^12.69.0", "exercised_package": "laravel/framework:12.69.0", "bootstrap": "fresh_supported_install"}, - {"laravel": "13", "php": "8.4", "package": "laravel/framework:^13.30.0", "exercised_package": "laravel/framework:13.30.0", "bootstrap": "fresh_supported_install"} + {"laravel": "12", "php": "8.2", "package": "laravel/framework:^12.72.0", "exercised_package": "laravel/framework:12.72.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "12", "php": "8.3", "package": "laravel/framework:^12.72.0", "exercised_package": "laravel/framework:12.72.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "13", "php": "8.3", "package": "laravel/framework:^13.41.0", "exercised_package": "laravel/framework:13.41.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "12", "php": "8.4", "package": "laravel/framework:^12.72.0", "exercised_package": "laravel/framework:12.72.0", "bootstrap": "fresh_supported_install"}, + {"laravel": "13", "php": "8.4", "package": "laravel/framework:^13.41.0", "exercised_package": "laravel/framework:13.41.0", "bootstrap": "fresh_supported_install"} ] }, "embedded_transition_authority": { diff --git a/docs/portal/frameworks/laravel.md b/docs/portal/frameworks/laravel.md index e5018a0f..5f9ba7b6 100644 --- a/docs/portal/frameworks/laravel.md +++ b/docs/portal/frameworks/laravel.md @@ -17,11 +17,16 @@ next:

Cloud or Server

durable-workflow/sdk connects the same Laravel application structure to a separately operated namespace. The Laravel bridge keeps container resolution, configuration, Artisan, logging, events, and testing.

-The service-mode bridge supports Laravel 12.69.0 and 13.30.0, and later -releases of each major (`^12.69.0|^13.30.0`). The Laravel bridge requires PHP -8.2 or newer, with PHP 8.3 or newer for Laravel 13. These are the same patched -framework and PHP floors published by Workflow 2.21.0 for embedded v2. The -bare HTTP SDK continues to support PHP 8.1. +The service-mode bridge supports Laravel 12.72.0 and 13.41.0, and later +releases of each major (`^12.72.0|^13.41.0`). The Laravel bridge requires PHP +8.2 or newer, with PHP 8.3 or newer for Laravel 13. These Laravel floors include +the fixes for [task output injection](https://github.com/advisories/GHSA-63hm-g96r-2m2q) +and both temporary signed-URL expiration bypasses +([query parsing](https://github.com/laravel/framework/security/advisories/GHSA-v5ch-g375-82r6), +[numeric route parameters](https://github.com/laravel/framework/security/advisories/GHSA-6rv8-qj5v-p8ch)). +Embedded-to-service transitions require the installed Workflow package to +publish the same Laravel and PHP support contract. The bare HTTP SDK continues +to support PHP 8.1. SDK 3.10 removes Laravel 10 and 11 from service-mode framework support. Those majors are past their upstream end of life and have published advisories with diff --git a/docs/portal/operate/upgrading.md b/docs/portal/operate/upgrading.md index 92dbe867..51f34582 100644 --- a/docs/portal/operate/upgrading.md +++ b/docs/portal/operate/upgrading.md @@ -13,12 +13,12 @@ next: This guide covers PHP SDK {{ release.sdkVersion }} and its changes listed in the [changelog](https://github.com/durable-workflow/sdk-php/blob/main/CHANGELOG.md). The bare SDK retains PHP `^8.1`, and Symfony 6.4/7/8 support is unchanged. -The Laravel bridge follows Workflow 2.21.0: PHP `^8.2` and patched Laravel -`^12.69.0|^13.30.0`, with PHP 8.3 or newer for Laravel 13. SDK 3.10 removes +The Laravel bridge requires PHP `^8.2` and patched Laravel +`^12.72.0|^13.41.0`, with PHP 8.3 or newer for Laravel 13. SDK 3.10 removes Laravel 10/11 bridge support. Its provider checks the installed framework and PHP before registering application bindings, including during auto-discovery. Upgrade the platform before adopting this bridge; existing SDK 3.9.x and -Workflow 2.20.x applications retain their own support contracts while upgrading. +Workflow 2.20.x applications must also apply Laravel security updates while upgrading. Stable-v1 authority remains unchanged. SDK 3.11 keeps these platform contracts and adds the occurrence-deadline rollout below. diff --git a/docs/sdk-reference.md b/docs/sdk-reference.md index 7c952af1..0c32a303 100644 --- a/docs/sdk-reference.md +++ b/docs/sdk-reference.md @@ -10,11 +10,12 @@ runnable path. - **Laravel adoption:** start with the [ownership-first transition guide](https://php.durable-workflow.com/frameworks/laravel/) when moving a Laravel Workflow v1 application to v2 embedded or service mode, or when - moving embedded v2 to service mode. The SDK 3.10 bridge and Workflow 2.21.0 - support PHP `^8.2` and patched Laravel `^12.69.0|^13.30.0`; Laravel 13 needs - PHP 8.3 or newer. The bare SDK retains PHP `^8.1`. Existing SDK 3.9.x and - Workflow 2.20.x applications keep their release's support contract while - upgrading PHP and Laravel. Stable-v1 authority is unchanged. + moving embedded v2 to service mode. The current bridge supports PHP `^8.2` + and patched Laravel `^12.72.0|^13.41.0`; Laravel 13 needs PHP 8.3 or newer. + Embedded-to-service transitions require the installed Workflow package to + publish that same support contract. The bare SDK retains PHP `^8.1`. + Existing applications must apply Laravel security updates while upgrading. + Stable-v1 authority is unchanged. - **Plain PHP service mode:** follow the quickstart below when a framework-neutral application and remote worker connect to Durable Workflow Cloud or a self-hosted Server. @@ -878,12 +879,11 @@ and the shipped [client](../examples/message-stream-client.php) and ## Laravel service mode -Patched Laravel `^12.69.0|^13.30.0` applications auto-discover the provider from +Patched Laravel `^12.72.0|^13.41.0` applications auto-discover the provider from the SDK package. The bridge requires PHP `^8.2`, with PHP 8.3 or newer for Laravel 13. The provider refuses unsupported or unknown installed framework/PHP versions before registering config, clients or workers. Upgrade PHP and Laravel -before adopting this bridge, or retain the existing SDK 3.9.x release while -upgrading the platform. The bare SDK's PHP `^8.1` support remains independent. +before adopting this bridge. The bare SDK's PHP `^8.1` support remains independent. Publish the environment-backed configuration, add attributed handler services, and start the supervised Artisan command. The worker process needs `ext-pcntl`; the command refuses to start without it so `SIGINT` and `SIGTERM` always drain diff --git a/examples/laravel-http/composer.json b/examples/laravel-http/composer.json index 584845b7..c6253e5e 100644 --- a/examples/laravel-http/composer.json +++ b/examples/laravel-http/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^8.3", "durable-workflow/sdk": "3.9.1", - "laravel/framework": "12.69.3", + "laravel/framework": "12.72.0", "laravel/octane": "2.21.0", "spiral/roadrunner-cli": "^2.6.0", "spiral/roadrunner-http": "^3.3.0" diff --git a/examples/laravel-http/composer.lock b/examples/laravel-http/composer.lock index 73da04a7..0d0f2d62 100644 --- a/examples/laravel-http/composer.lock +++ b/examples/laravel-http/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "59bf5010338cae5204f26885ef9f5b62", + "content-hash": "841dce9888a700b2c79988a01e6fb4eb", "packages": [ { "name": "apache/avro", @@ -1470,16 +1470,16 @@ }, { "name": "laravel/framework", - "version": "v12.69.3", + "version": "v12.72.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717" + "reference": "b089d3020e508f49b3625726ab4a2c897f1b78d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/58ea544a2a80dc03c168e13a5dc9a1d176a88717", - "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "url": "https://api.github.com/repos/laravel/framework/zipball/b089d3020e508f49b3625726ab4a2c897f1b78d5", + "reference": "b089d3020e508f49b3625726ab4a2c897f1b78d5", "shasum": "" }, "require": { @@ -1592,10 +1592,10 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.9.0", + "orchestra/testbench-core": "^10.15.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.1.41", + "phpstan/phpstan": "2.2.13", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", "resend/resend-php": "^0.10.0|^1.0", @@ -1688,7 +1688,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-09-29T12:53:25+00:00" + "time": "2026-11-10T15:45:11+00:00" }, { "name": "laravel/octane", diff --git a/src/Bridge/Laravel/LaravelPlatform.php b/src/Bridge/Laravel/LaravelPlatform.php index 16ac30ca..c217b309 100644 --- a/src/Bridge/Laravel/LaravelPlatform.php +++ b/src/Bridge/Laravel/LaravelPlatform.php @@ -10,8 +10,8 @@ final class LaravelPlatform { private const PLATFORMS = [ - 12 => ['floor' => '12.69.0', 'php' => 80200], - 13 => ['floor' => '13.30.0', 'php' => 80300], + 12 => ['floor' => '12.72.0', 'php' => 80200], + 13 => ['floor' => '13.41.0', 'php' => 80300], ]; public static function laravelConstraint(): string @@ -41,7 +41,7 @@ public static function assertSupported(?string $laravelVersion, int $phpVersionI throw new RuntimeException( 'The Durable Workflow Laravel bridge requires PHP ^8.2 and Laravel '.self::laravelConstraint() .'; Laravel 13 requires PHP ^8.3. Upgrade the application platform before activating the bridge' - .' or retain the existing SDK 3.9.x release while upgrading.', + .'. The bare HTTP client remains available independently of this optional bridge.', ); } } diff --git a/tests/FrameworkBridgeTest.php b/tests/FrameworkBridgeTest.php index a2605399..f05ebd16 100644 --- a/tests/FrameworkBridgeTest.php +++ b/tests/FrameworkBridgeTest.php @@ -277,7 +277,7 @@ public function testComposerMetadataAdvertisesOptionalSupportedFrameworks(): voi $manifest['extra']['laravel']['providers'], ); self::assertSame( - '^12.69.0|^13.30.0', + '^12.72.0|^13.41.0', $manifest['extra']['durable-workflow']['frameworks']['laravel'], ); self::assertSame('^6.4|^7.0|^8.0', $manifest['extra']['durable-workflow']['frameworks']['symfony']); diff --git a/tests/LaravelAdoptionContractTest.php b/tests/LaravelAdoptionContractTest.php index 74bbc733..7d401fc6 100644 --- a/tests/LaravelAdoptionContractTest.php +++ b/tests/LaravelAdoptionContractTest.php @@ -32,11 +32,11 @@ public function testBridgeCompatibilityMatchesTheEmbeddedLaravelSurface(): void 'The optional Laravel bridge floor must not raise the bare HTTP SDK floor.'); self::assertSame( [ - ['laravel' => '12', 'php' => '8.2', 'package' => 'laravel/framework:^12.69.0', 'exercised_package' => 'laravel/framework:12.69.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '12', 'php' => '8.3', 'package' => 'laravel/framework:^12.69.0', 'exercised_package' => 'laravel/framework:12.69.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '13', 'php' => '8.3', 'package' => 'laravel/framework:^13.30.0', 'exercised_package' => 'laravel/framework:13.30.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '12', 'php' => '8.4', 'package' => 'laravel/framework:^12.69.0', 'exercised_package' => 'laravel/framework:12.69.0', 'bootstrap' => 'fresh_supported_install'], - ['laravel' => '13', 'php' => '8.4', 'package' => 'laravel/framework:^13.30.0', 'exercised_package' => 'laravel/framework:13.30.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '12', 'php' => '8.2', 'package' => 'laravel/framework:^12.72.0', 'exercised_package' => 'laravel/framework:12.72.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '12', 'php' => '8.3', 'package' => 'laravel/framework:^12.72.0', 'exercised_package' => 'laravel/framework:12.72.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '13', 'php' => '8.3', 'package' => 'laravel/framework:^13.41.0', 'exercised_package' => 'laravel/framework:13.41.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '12', 'php' => '8.4', 'package' => 'laravel/framework:^12.72.0', 'exercised_package' => 'laravel/framework:12.72.0', 'bootstrap' => 'fresh_supported_install'], + ['laravel' => '13', 'php' => '8.4', 'package' => 'laravel/framework:^13.41.0', 'exercised_package' => 'laravel/framework:13.41.0', 'bootstrap' => 'fresh_supported_install'], ], $this->contract['framework']['qualification_matrix'], ); @@ -48,13 +48,13 @@ public function testBridgeCompatibilityMatchesTheEmbeddedLaravelSurface(): void ], $this->contract['embedded_transition_authority']); } - public function testServiceModePublishesThePatchedLaravelFloorsForGhsaF9x37vq9Cph6(): void + public function testServiceModePublishesThePatchedLaravelFloorsForGhsa63hmG96r2m2q(): void { - // The maintained majors and patched floors published by Workflow 2.21.0. - $floors = '^12.69.0|^13.30.0'; + // Current maintained majors and the CVE-2026-128083 patched floors. + $floors = '^12.72.0|^13.41.0'; self::assertSame($floors, $this->contract['framework']['constraint']); self::assertSame($floors, $this->composer['extra']['durable-workflow']['frameworks']['laravel']); - foreach (['12.69.0', '13.30.0'] as $floor) { + foreach (['12.72.0', '13.41.0'] as $floor) { self::assertStringContainsString($floor, $this->composer['suggest']['laravel/framework']); } self::assertArrayNotHasKey('laravel/framework', $this->composer['conflict'] ?? [], @@ -133,16 +133,16 @@ static function (string $workflow) use ($search, $replace): string { " php: '8.4'\n", ), 'A framework cell must use its job\'s fixed PHP minor.']; yield 'exact minimum removed from a cell' => [$first( - " exercised_package: 'laravel/framework:12.69.0'\n", + " exercised_package: 'laravel/framework:12.72.0'\n", '', ), 'The CI Laravel cells differ from the adoption contract.']; yield 'cell exercises a later 12.x release' => [$first( - "exercised_package: 'laravel/framework:12.69.0'", - "exercised_package: 'laravel/framework:12.70.0'", + "exercised_package: 'laravel/framework:12.72.0'", + "exercised_package: 'laravel/framework:12.73.0'", ), 'The CI Laravel cells differ from the adoption contract.']; yield 'cell exercises a caret range' => [$first( - "exercised_package: 'laravel/framework:12.69.0'", - "exercised_package: 'laravel/framework:^12.69.0'", + "exercised_package: 'laravel/framework:12.72.0'", + "exercised_package: 'laravel/framework:^12.72.0'", ), 'The CI Laravel cells differ from the adoption contract.']; yield 'no installed-version assertion after the SDK' => [static fn (string $workflow): string => str_replace( " --no-interaction --prefer-dist --no-progress\n" diff --git a/tests/LaravelPlatformTest.php b/tests/LaravelPlatformTest.php index 7f585275..19735423 100644 --- a/tests/LaravelPlatformTest.php +++ b/tests/LaravelPlatformTest.php @@ -44,25 +44,32 @@ public function testPlatformAdmissionIsClosed(?string $version, int $php, bool $ /** @return iterable */ public static function platforms(): iterable { - yield 'Laravel 12 floor' => ['12.69.0.0', 80200, true]; - yield 'Laravel 12 later patch' => ['12.69.3.0', 80400, true]; - yield 'Laravel 12 stable version' => ['12.69.0', 80300, true]; - yield 'Laravel 13 floor' => ['13.30.0.0', 80300, true]; - yield 'Laravel 13 later release' => ['13.31.0.0', 80400, true]; + yield 'Laravel 12 floor' => ['12.72.0.0', 80200, true]; + yield 'Laravel 12 later patch' => ['12.72.1.0', 80400, true]; + yield 'Laravel 12 stable version' => ['12.72.0', 80300, true]; + yield 'Laravel 13 floor' => ['13.41.0.0', 80300, true]; + yield 'Laravel 13 later release' => ['13.42.0.0', 80400, true]; yield 'removed Laravel 10' => ['10.50.4.0', 80400, false]; yield 'removed Laravel 11' => ['11.56.1.0', 80400, false]; yield 'unqualified future major' => ['14.0.0.0', 80400, false]; - yield 'Laravel 12 before patch' => ['12.68.99.0', 80400, false]; - yield 'Laravel 13 before patch' => ['13.29.99.0', 80400, false]; - yield 'bridge PHP before floor' => ['12.69.0.0', 80199, false]; - yield 'Laravel 13 PHP before floor' => ['13.30.0.0', 80299, false]; - yield 'unsupported PHP major' => ['13.30.0.0', 90000, false]; + yield 'Laravel 12 before patch' => ['12.71.99.0', 80400, false]; + yield 'Laravel 13 before patch' => ['13.40.99.0', 80400, false]; + yield 'previous Laravel 12 floor is affected' => ['12.69.0.0', 80400, false]; + yield 'previous example Laravel is affected' => ['12.69.3.0', 80400, false]; + yield 'last Laravel 12 release before fix' => ['12.70.2.0', 80400, false]; + yield 'previous Laravel 13 floor is affected' => ['13.30.0.0', 80400, false]; + yield 'last Laravel 13 release before fix' => ['13.39.0.0', 80400, false]; + yield 'concurrency-only Laravel 12 fix lacks signed-URL fixes' => ['12.71.0.0', 80400, false]; + yield 'concurrency-only Laravel 13 fix lacks signed-URL fixes' => ['13.40.0.0', 80400, false]; + yield 'bridge PHP before floor' => ['12.72.0.0', 80199, false]; + yield 'Laravel 13 PHP before floor' => ['13.41.0.0', 80299, false]; + yield 'unsupported PHP major' => ['13.41.0.0', 90000, false]; yield 'missing framework version' => [null, 80400, false]; yield 'empty framework version' => ['', 80400, false]; yield 'unresolved development version' => ['12.x-dev', 80400, false]; - yield 'unqualified prerelease' => ['13.30.0.0-beta', 80400, false]; - yield 'unrecognized version spelling' => ['v12.69.0', 80400, false]; - yield 'version controls' => ["12.69.0\n", 80400, false]; + yield 'unqualified prerelease' => ['13.41.0.0-beta', 80400, false]; + yield 'unrecognized version spelling' => ['v12.72.0', 80400, false]; + yield 'version controls' => ["12.72.0\n", 80400, false]; } #[RunInSeparateProcess] @@ -100,7 +107,9 @@ public function testAutoDiscoveryRefusesBeforeConfigOrBindings(?string $version) public static function rejectedProviderVersions(): iterable { yield 'old supported major removed' => ['11.56.1.0']; - yield 'supported major below patch' => ['12.68.99.0']; + yield 'supported major below patch' => ['12.70.99.0']; + yield 'previous example below security floor' => ['12.69.3.0']; + yield 'previous Laravel 13 floor' => ['13.30.0.0']; yield 'unknown installed framework' => [null]; } } diff --git a/tests/LaravelTransitionAuthorityTest.php b/tests/LaravelTransitionAuthorityTest.php index 7bc7c7a2..2648e00a 100644 --- a/tests/LaravelTransitionAuthorityTest.php +++ b/tests/LaravelTransitionAuthorityTest.php @@ -9,7 +9,7 @@ /** * The embedded-v2 to service-mode transition check CI runs against the - * installed Workflow package. The positive fixture is published Workflow 2.21.0's + * installed Workflow package. The historical fixture is published Workflow 2.21.0's * resources/laravel-embedded-upgrade-contract.json, byte for byte * (SHA-256 1e65a6ffa6f2a1feba53ff0b5f7782a9ac8d7a3560f31c44c040767c592308c6). */ @@ -35,10 +35,28 @@ protected function tearDown(): void rmdir($this->directory); } - public function testPublishedServiceAuthorityEqualsWorkflowEmbeddedAuthority(): void + public function testHistoricalPublishedAuthorityCannotAuthorizeTheNewPatchedServiceFloor(): void { [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json'); + self::assertSame(1, $status); + self::assertStringContainsString('Embedded and service modes publish different Laravel constraints.', $errors); + } + + public function testHistoricalMatchingSupportStillPassesTheUnchangedChecker(): void + { + // This is an inert historical service-contract copy. It cannot qualify + // the current SDK floor or replace the installed corrected producer gate. + $contract = $this->contract(); + $contract['framework']['constraint'] = '^12.69.0|^13.30.0'; + foreach ($contract['framework']['qualification_matrix'] as $index => $cell) { + $floor = $cell['laravel'] === '12' ? '12.69.0' : '13.30.0'; + $contract['framework']['qualification_matrix'][$index]['package'] = 'laravel/framework:^'.$floor; + $contract['framework']['qualification_matrix'][$index]['exercised_package'] = 'laravel/framework:'.$floor; + } + $path = $this->directory.'/historical-service.json'; + file_put_contents($path, json_encode($contract, JSON_THROW_ON_ERROR)); + [$status, $errors] = $this->check($path); self::assertSame('', $errors); self::assertSame(0, $status); } @@ -101,17 +119,17 @@ public static function counterfactuals(): iterable return $contract; }, 'Laravel 12 on PHP 8.2 does not qualify the published floor.']; yield 'cell exercises a later release than its floor' => [static function (array $contract): array { - $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:12.70.0'; + $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:12.73.0'; return $contract; }, 'Laravel 12 on PHP 8.2 does not exercise exactly the published floor.']; yield 'cell exercises its caret range' => [static function (array $contract): array { - $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:^12.69.0'; + $contract['framework']['qualification_matrix'][0]['exercised_package'] = 'laravel/framework:^12.72.0'; return $contract; }, 'Laravel 12 on PHP 8.2 does not exercise exactly the published floor.']; yield 'pre-fix floor' => [static function (array $contract): array { - $contract['framework']['constraint'] = '^12.68.0|^13.30.0'; + $contract['framework']['constraint'] = '^12.68.0|^13.41.0'; return $contract; }, 'Embedded and service modes publish different Laravel constraints.']; diff --git a/tests/StaticCredentialSubjectTest.php b/tests/StaticCredentialSubjectTest.php index c82eb98f..f73d92b1 100644 --- a/tests/StaticCredentialSubjectTest.php +++ b/tests/StaticCredentialSubjectTest.php @@ -23,9 +23,9 @@ public function testDependencyIntentHasExactVersionsAndFutureInputs(): void self::assertSame('3.10.0', $intent['requirements']['durable-workflow/sdk']); // The selected #307 graph: FrameworkBundle 7.4.21 is refused by advisory // GHSA-xg2q-4rrq-657q, so the intent names the patched 7.4.23 release. - self::assertSame(['php' => '8.4.27', 'durable-workflow/sdk' => '3.10.0', 'laravel/framework' => '12.69.3', + self::assertSame(['php' => '8.4.27', 'durable-workflow/sdk' => '3.10.0', 'laravel/framework' => '12.72.0', 'symfony/framework-bundle' => '7.4.23'], $intent['requirements']); - self::assertSame(['laravel/framework' => '58ea544a2a80dc03c168e13a5dc9a1d176a88717', + self::assertSame(['laravel/framework' => 'b089d3020e508f49b3625726ab4a2c897f1b78d5', 'symfony/framework-bundle' => '6a749f2c37cb9def53793633ec72958aaad4b359'], $intent['framework_source_references']); self::assertCount(3, $intent['future_inputs']); self::assertSame(['plugins' => false, 'scripts' => false, 'advisory_policy' => 'normal'], $intent['dependency_policy']); diff --git a/tests/fixtures/static-credential-consumer/dependency-intent.json b/tests/fixtures/static-credential-consumer/dependency-intent.json index 401da276..aa770764 100644 --- a/tests/fixtures/static-credential-consumer/dependency-intent.json +++ b/tests/fixtures/static-credential-consumer/dependency-intent.json @@ -3,11 +3,11 @@ "requirements": { "php": "8.4.27", "durable-workflow/sdk": "3.10.0", - "laravel/framework": "12.69.3", + "laravel/framework": "12.72.0", "symfony/framework-bundle": "7.4.23" }, "framework_source_references": { - "laravel/framework": "58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "laravel/framework": "b089d3020e508f49b3625726ab4a2c897f1b78d5", "symfony/framework-bundle": "6a749f2c37cb9def53793633ec72958aaad4b359" }, "fixture": { From f8d9f013242604b6a9b415efa1fe3caa979cef6a Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:46:29 +0000 Subject: [PATCH 2/5] Keep advisory links on separate rows in the Laravel guide --- docs/portal/frameworks/laravel.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/portal/frameworks/laravel.md b/docs/portal/frameworks/laravel.md index 5f9ba7b6..105540b2 100644 --- a/docs/portal/frameworks/laravel.md +++ b/docs/portal/frameworks/laravel.md @@ -20,10 +20,12 @@ next: The service-mode bridge supports Laravel 12.72.0 and 13.41.0, and later releases of each major (`^12.72.0|^13.41.0`). The Laravel bridge requires PHP 8.2 or newer, with PHP 8.3 or newer for Laravel 13. These Laravel floors include -the fixes for [task output injection](https://github.com/advisories/GHSA-63hm-g96r-2m2q) -and both temporary signed-URL expiration bypasses -([query parsing](https://github.com/laravel/framework/security/advisories/GHSA-v5ch-g375-82r6), -[numeric route parameters](https://github.com/laravel/framework/security/advisories/GHSA-6rv8-qj5v-p8ch)). +the November security fixes: + +- [Task output injection](https://github.com/advisories/GHSA-63hm-g96r-2m2q) +- [Signed URL query limits](https://github.com/laravel/framework/security/advisories/GHSA-v5ch-g375-82r6) +- [Signed URL route parameters](https://github.com/laravel/framework/security/advisories/GHSA-6rv8-qj5v-p8ch) + Embedded-to-service transitions require the installed Workflow package to publish the same Laravel and PHP support contract. The bare HTTP SDK continues to support PHP 8.1. From 6d33d1acbfd00cdd98c1b742d307037da1e5092b Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:52:31 +0000 Subject: [PATCH 3/5] Keep the historical deadline release separate from current Laravel floors --- docs/portal/operate/upgrading.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/portal/operate/upgrading.md b/docs/portal/operate/upgrading.md index 51f34582..54b445ac 100644 --- a/docs/portal/operate/upgrading.md +++ b/docs/portal/operate/upgrading.md @@ -19,8 +19,8 @@ Laravel 10/11 bridge support. Its provider checks the installed framework and PHP before registering application bindings, including during auto-discovery. Upgrade the platform before adopting this bridge; existing SDK 3.9.x and Workflow 2.20.x applications must also apply Laravel security updates while upgrading. -Stable-v1 authority remains unchanged. SDK 3.11 keeps these platform contracts -and adds the occurrence-deadline rollout below. +Stable-v1 authority remains unchanged. SDK 3.11 adds the occurrence-deadline +rollout below. ## Condition-wait occurrence deadlines From a2444161981b1c3d028aed8b082c3b8f8ec1f391 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:03:17 +0000 Subject: [PATCH 4/5] Prepare the SDK 3.18.3 package identity for the Laravel security patch --- composer.json | 2 +- docs/quickstart-contract.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 91ee9ac0..7f3dee69 100644 --- a/composer.json +++ b/composer.json @@ -97,7 +97,7 @@ } }, "durable-workflow": { - "product-train": "3.18.2", + "product-train": "3.18.3", "supported-server-versions": "2.5.0", "supported-server-release-baselines": { "2": "2.5.0", diff --git a/docs/quickstart-contract.json b/docs/quickstart-contract.json index dc8da1d7..a49992b2 100644 --- a/docs/quickstart-contract.json +++ b/docs/quickstart-contract.json @@ -3,8 +3,8 @@ "schema_version": 2, "package": { "name": "durable-workflow/sdk", - "published_version": "3.18.2", - "composer_requirement": "3.18.2", + "published_version": "3.18.3", + "composer_requirement": "3.18.3", "onboarding_requirement": "^3.18" }, "runtime_targets": { From f3364f5eaddd46d6ea48c92d6e49125597fccd0d Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 14 Nov 2026 04:08:21 +0000 Subject: [PATCH 5/5] Consume published Workflow 2.28.1 transition authority --- tests/LaravelTransitionAuthorityTest.php | 29 +++- .../laravel-embedded-upgrade-contract.json | 128 ++++++++++++++++++ 2 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/laravel-transition/workflow-2.28.1/resources/laravel-embedded-upgrade-contract.json diff --git a/tests/LaravelTransitionAuthorityTest.php b/tests/LaravelTransitionAuthorityTest.php index 2648e00a..95a134c9 100644 --- a/tests/LaravelTransitionAuthorityTest.php +++ b/tests/LaravelTransitionAuthorityTest.php @@ -9,9 +9,9 @@ /** * The embedded-v2 to service-mode transition check CI runs against the - * installed Workflow package. The historical fixture is published Workflow 2.21.0's - * resources/laravel-embedded-upgrade-contract.json, byte for byte - * (SHA-256 1e65a6ffa6f2a1feba53ff0b5f7782a9ac8d7a3560f31c44c040767c592308c6). + * installed Workflow package. Versioned fixtures retain the exact published + * resources/laravel-embedded-upgrade-contract.json bytes. Historical authorities + * cannot qualify a newer floor; Workflow 2.28.1 supplies the patched boundary. */ final class LaravelTransitionAuthorityTest extends TestCase { @@ -19,6 +19,8 @@ final class LaravelTransitionAuthorityTest extends TestCase private const WORKFLOW_2_21_0 = __DIR__.'/fixtures/laravel-transition/workflow-2.21.0'; + private const WORKFLOW_2_28_1 = __DIR__.'/fixtures/laravel-transition/workflow-2.28.1'; + private string $directory; protected function setUp(): void @@ -37,7 +39,7 @@ protected function tearDown(): void public function testHistoricalPublishedAuthorityCannotAuthorizeTheNewPatchedServiceFloor(): void { - [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json'); + [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json', self::WORKFLOW_2_21_0); self::assertSame(1, $status); self::assertStringContainsString('Embedded and service modes publish different Laravel constraints.', $errors); @@ -56,7 +58,7 @@ public function testHistoricalMatchingSupportStillPassesTheUnchangedChecker(): v } $path = $this->directory.'/historical-service.json'; file_put_contents($path, json_encode($contract, JSON_THROW_ON_ERROR)); - [$status, $errors] = $this->check($path); + [$status, $errors] = $this->check($path, self::WORKFLOW_2_21_0); self::assertSame('', $errors); self::assertSame(0, $status); } @@ -72,6 +74,21 @@ public function testPublishedAuthorityFixtureRetainsItsExactPackageBytesAndStabl $published['supported_intersection']['authority']['stable_v1']); } + public function testCurrentPublishedAuthorityMatchesThePatchedServiceBoundary(): void + { + $path = self::WORKFLOW_2_28_1.'/resources/laravel-embedded-upgrade-contract.json'; + self::assertSame('153a261371272e0fa8e0a6e79dbe425b9adc311cbe596ce0237575a78b0828a7', hash_file('sha256', $path)); + $published = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); + $historical = json_decode((string) file_get_contents(self::WORKFLOW_2_21_0.'/resources/laravel-embedded-upgrade-contract.json'), + true, 512, JSON_THROW_ON_ERROR); + self::assertSame($historical['supported_intersection']['authority']['stable_v1'], + $published['supported_intersection']['authority']['stable_v1']); + + [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json'); + self::assertSame('', $errors); + self::assertSame(0, $status); + } + public function testPreviousEmbeddedSupportBoundaryDoesNotAuthorizeTheNewServiceFloor(): void { [$status, $errors] = $this->check(dirname(__DIR__).'/docs/laravel-adoption-contract.json', self::WORKFLOW_2_17_1); @@ -159,7 +176,7 @@ private function contract(): array } /** @return array{int, string} */ - private function check(string $contract, string $workflow = self::WORKFLOW_2_21_0): array + private function check(string $contract, string $workflow = self::WORKFLOW_2_28_1): array { $process = proc_open([PHP_BINARY, dirname(__DIR__).'/scripts/ci/check-laravel-transition-authority.php', $contract, $workflow], [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); diff --git a/tests/fixtures/laravel-transition/workflow-2.28.1/resources/laravel-embedded-upgrade-contract.json b/tests/fixtures/laravel-transition/workflow-2.28.1/resources/laravel-embedded-upgrade-contract.json new file mode 100644 index 00000000..b999b330 --- /dev/null +++ b/tests/fixtures/laravel-transition/workflow-2.28.1/resources/laravel-embedded-upgrade-contract.json @@ -0,0 +1,128 @@ +{ + "schema": "durable-workflow.laravel-embedded-upgrade.contract", + "version": 1, + "transition": { + "from": { + "package": "durable-workflow/workflow", + "channel": "^1.0", + "engine": "v1" + }, + "to": { + "package": "durable-workflow/workflow", + "channel": "^2.0@rc", + "engine": "embedded_v2" + }, + "legacy_package_alias": { + "package": "laravel-workflow/laravel-workflow", + "role": "composer_replace_only" + }, + "deployment_owner": "laravel_application", + "requires_service_mode": false + }, + "supported_intersection": { + "authority": { + "stable_v1": { + "php": "^8.1", + "laravel": "^9.0|^10.0|^11.0|^12.0|^13.0" + }, + "embedded_v2": { + "php": "^8.2", + "laravel": "^12.72.0|^13.41.0" + }, + "qualified_php_minors": ["8.2", "8.3", "8.4"], + "laravel_minimum_php": { + "12.*": "8.2", + "13.*": "8.3" + } + }, + "cells": [ + {"php": "8.2", "laravel": "12.*"}, + {"php": "8.3", "laravel": "12.*"}, + {"php": "8.3", "laravel": "13.*"}, + {"php": "8.4", "laravel": "12.*"}, + {"php": "8.4", "laravel": "13.*"} + ] + }, + "state_ownership": { + "v1": { + "engine": "v1", + "tables": [ + "workflows", + "workflow_logs", + "workflow_signals", + "workflow_timers", + "workflow_exceptions", + "workflow_relationships" + ], + "history_policy": "finish_on_v1", + "reinterpret_as_v2": false + }, + "embedded_v2": { + "engine": "embedded_v2", + "root_tables": [ + "workflow_instances", + "workflow_runs", + "workflow_history_events", + "workflow_tasks" + ], + "accepts_v1_history": false + }, + "composer_change_migrates_history": false + }, + "strategies": { + "drain": { + "status_command": "php artisan workflow:v2:upgrade-status --strategy=drain --json", + "new_v1_starts": "stop", + "new_v2_starts": "after_v1_open_runs_reach_zero" + }, + "coexist": { + "status_command": "php artisan workflow:v2:upgrade-status --strategy=coexist --json", + "v2_queue_config": "workflows.v2.queue", + "v2_queue_env": "DW_V2_QUEUE", + "queue_isolation_required": true, + "new_starts": "embedded_v2" + } + }, + "continuity": { + "workflow_type": "explicit_v2_type_key", + "workflow_id": "application_owned_logical_identity", + "payloads": "adapt_application_inputs_to_v2_avro_values", + "queues": "route_by_engine_during_coexistence", + "memo_and_search_metadata": "set_on_v2_start_options", + "signals": "route_to_the_engine_that_owns_the_run", + "updates": "v2_runs_only", + "rollback": { + "composer_command": "composer require durable-workflow/workflow:^1.0 --with-all-dependencies", + "state_policy": "restore_v1_code_and_workers_for_v1_runs_without_replaying_v2_history" + }, + "durable_state_transfer": "none" + }, + "laravel_experience": { + "straight_line_authoring": true, + "constructor_injection": true, + "handle_method_injection": true, + "application_config": "workflows", + "queue_runtime": "laravel_queue", + "logging": "laravel_log", + "events": "laravel_events", + "testing_fake": "Workflow\\V2\\WorkflowStub::fake", + "testing_assertions": [ + "Workflow\\V2\\WorkflowStub::assertDispatched", + "Workflow\\V2\\WorkflowStub::assertSignalSent", + "Workflow\\V2\\WorkflowStub::assertUpdateSent" + ] + }, + "journey": { + "stable_docs": "/docs/migration", + "v2_docs": "/docs/2.0/migration", + "php_sdk_laravel": "https://github.com/durable-workflow/sdk-php#laravel-service-mode" + }, + "qualification": { + "source_script": "scripts/ci/laravel-embedded-upgrade-smoke.sh", + "dependency_security_policy": "resources/laravel-dependency-security-policy.json", + "published_artifacts_required": true, + "clean_application_required": true, + "application_bootstrap_patch_allowed": false, + "eol_framework_fixture_policy": "composer_no_blocking_only_for_disposable_compatibility_qualification" + } +}