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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The bare `env-kit` rate limiter. It is still registered, delegates to the same limit, and logs a
warning naming `laranail-env-kit-webui.api`. Earliest removal: the next minor after 0.1.

### Fixed

- The deprecated-name warnings fired on every resolution, so a page with N links to a bare
`env-kit.*` route name logged N lines per request, and the bare `env-kit` limiter logged once per
throttled request for the life of an Octane or queue worker. Each deprecated name now warns once
for the life of the booted application.
- The suite configured the engine through the bare `env-kit.*` keys (`path`, `backup_path`,
`audit.enabled`, `auto_backup`, `hidden_keys`, `editable_keys`, `schema`,
`limits.max_value_length`). `laranail/env-kit` now reads its configuration only at
`laranail.env-kit.*`, so against a fresh resolve every one of those overrides was ignored and 13
tests ran against the defaults: no temp `.env`, no schema, no allowlist. The tests, and the
`docs/api.md` mention of the schema key, now use `laranail.env-kit.*`. No runtime code read the
bare keys.

## [0.1.0] - 2026-07-11

### Fixed
Expand Down
5 changes: 3 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,16 @@ route('laranail-env-kit-webui.keys.show', ['key' => 'APP_NAME']);

> **Deprecated aliases.** The bare names these replaced — `env-kit.keys.*`, `env-kit.panel` and
> the `env-kit` limiter — still work: `route('env-kit.panel')` resolves to the scoped route and
> `throttle:env-kit` applies the same limit, each logging a warning that names the replacement.
> `throttle:env-kit` applies the same limit, each logging a warning that names the replacement
> (once per name for the life of the booted application, not once per link or request).
> They are removed no earlier than the next minor after 0.1. Two things do not follow the alias:
> `Route::has('env-kit.panel')` and `request()->routeIs('env-kit.*')` ask the route collection
> directly, so use the scoped names there.

## Validation & status codes

Input is validated with the **headless rules** (`ValidEnvKey`, `ValidEnvValue`,
and `MatchesEnvSchema` — so a configured `env-kit.schema` is enforced over the API
and `MatchesEnvSchema` — so a configured `laranail.env-kit.schema` is enforced over the API
exactly as on the CLI; it is a no-op until a schema is defined) before the engine is
touched, and engine guards map to HTTP statuses:

Expand Down
32 changes: 28 additions & 4 deletions src/Providers/EnvKitWebUIServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@

final class EnvKitWebUIServiceProvider extends PackageServiceProvider
{
/**
* Deprecated names already warned about. Held for the life of the booted
* application -- one request under PHP-FPM, one worker under Octane or a
* queue worker -- so a page with N links to a bare route name logs one
* line, not N.
*
* @var array<string, true>
*/
private array $warnedDeprecations = [];

public function configurePackage(Package $package): void
{
$package
Expand Down Expand Up @@ -96,8 +106,8 @@ private function registerThrottle(): void
* same limit and logs a warning naming the replacement. Earliest
* removal: the next minor after 0.1.
*/
RateLimiter::for(RegisteredNames::LEGACY_LIMITER, static function (Request $request) use ($limit): Limit {
Log::warning(sprintf(
RateLimiter::for(RegisteredNames::LEGACY_LIMITER, function (Request $request) use ($limit): Limit {
$this->warnDeprecatedOnce('limiter:' . RegisteredNames::LEGACY_LIMITER, sprintf(
'[laranail/env-kit-webui] The rate limiter "%s" is deprecated; use "throttle:%s" instead.',
RegisteredNames::LEGACY_LIMITER,
RegisteredNames::LIMITER,
Expand Down Expand Up @@ -127,11 +137,11 @@ private function resolveBareRouteNames(): void
$previous = (fn (): mixed => $this->missingNamedRouteResolver)->call($url);

URL::resolveMissingNamedRoutesUsing(
static function (string $name, mixed $parameters, ?bool $absolute) use ($previous): ?string {
function (string $name, mixed $parameters, ?bool $absolute) use ($previous): ?string {
$scoped = RegisteredNames::scopedRouteFor($name);

if ($scoped !== null && Route::has($scoped)) {
Log::warning(sprintf(
$this->warnDeprecatedOnce('route:' . $name, sprintf(
'[laranail/env-kit-webui] The route name "%s" is deprecated; use "%s" instead.',
$name,
$scoped,
Expand All @@ -151,6 +161,20 @@ static function (string $name, mixed $parameters, ?bool $absolute) use ($previou
);
}

/**
* Log a deprecation once per name for the life of the booted application.
*/
private function warnDeprecatedOnce(string $key, string $message): void
{
if (isset($this->warnedDeprecations[$key])) {
return;
}

$this->warnedDeprecations[$key] = true;

Log::warning($message);
}

/**
* @param list<string> $fallbackMiddleware
* @param list<string> $prepend
Expand Down
2 changes: 1 addition & 1 deletion tests/Feature/LivewireTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
});

it('surfaces a guard failure inline instead of crashing', function () {
$this->bindEnv("DB_PASSWORD=secret\n", ['env-kit.auto_backup' => false]);
$this->bindEnv("DB_PASSWORD=secret\n", ['laranail.env-kit.auto_backup' => false]);

Livewire::test(EnvKitPanelComponent::class)
->call('startEditing', 'DB_PASSWORD') // protected key
Expand Down
32 changes: 32 additions & 0 deletions tests/Feature/NamingConventionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,35 @@ function registeredLimiterNames(): array
->withArgs(static fn (string $message): bool => str_contains($message, RegisteredNames::LIMITER))
->atLeast()->once();
});

it('warns once per deprecated route name, however many links resolve it', function (): void {
Log::spy();

// A page with N links to the same bare name must not log N lines.
foreach (range(1, 3) as $ignored) {
route('env-kit.panel');
route('env-kit.keys.index');
}

Log::shouldHaveReceived('warning')
->withArgs(static fn (string $message): bool => str_contains($message, '"env-kit.panel"'))
->once();
Log::shouldHaveReceived('warning')
->withArgs(static fn (string $message): bool => str_contains($message, '"env-kit.keys.index"'))
->once();
});

it('warns once for the deprecated env-kit limiter, however many requests it throttles', function (): void {
$legacy = app(RateLimiter::class)->limiter(RegisteredNames::LEGACY_LIMITER);
$request = Illuminate\Http\Request::create('/');

Log::spy();

foreach (range(1, 3) as $ignored) {
$legacy($request);
}

Log::shouldHaveReceived('warning')
->withArgs(static fn (string $message): bool => str_contains($message, RegisteredNames::LEGACY_LIMITER))
->once();
});
4 changes: 2 additions & 2 deletions tests/Feature/PolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

it('honours a custom hidden_keys pattern in the API (masking)', function () {
$this->bindEnv("APP_NAME=Acme\nWIDGET_API=topsecret123\n");
config(['env-kit.hidden_keys' => ['WIDGET_*']]);
config(['laranail.env-kit.hidden_keys' => ['WIDGET_*']]);

$response = $this->getJson('api/v1/env-kit/keys')->assertOk();

Expand All @@ -21,7 +21,7 @@

it('returns 403 for a write outside the editable allowlist', function () {
$this->bindEnv("A=1\n");
config(['env-kit.editable_keys' => ['APP_*']]);
config(['laranail.env-kit.editable_keys' => ['APP_*']]);

$this->postJson('api/v1/env-kit/keys', ['key' => 'DB_HOST', 'value' => 'x'])->assertForbidden();

Expand Down
4 changes: 2 additions & 2 deletions tests/Feature/SchemaValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

it('rejects a stored value that violates the engine schema', function () {
$this->bindEnv("A=1\n");
config(['env-kit.schema' => ['PORT' => 'integer']]);
config(['laranail.env-kit.schema' => ['PORT' => 'integer']]);

$this->postJson('api/v1/env-kit/keys', ['key' => 'PORT', 'value' => 'not-an-int'])
->assertStatus(422)
Expand All @@ -24,7 +24,7 @@

it('validates an updated value against the schema', function () {
$this->bindEnv("PORT=8080\n");
config(['env-kit.schema' => ['PORT' => 'integer']]);
config(['laranail.env-kit.schema' => ['PORT' => 'integer']]);

$this->putJson('api/v1/env-kit/keys/PORT', ['value' => 'nope'])
->assertStatus(422)
Expand Down
2 changes: 1 addition & 1 deletion tests/Feature/WebUiCoverageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

it('maps an engine validation failure (value too long) to 422', function () {
$this->bindEnv("A=1\n");
config(['laranail.env-kit-webui.enabled' => true, 'env-kit.limits.max_value_length' => 5]);
config(['laranail.env-kit-webui.enabled' => true, 'laranail.env-kit.limits.max_value_length' => 5]);

$this->postJson('api/v1/env-kit/keys', ['key' => 'NEW', 'value' => 'waytoolong'])->assertStatus(422);
});
10 changes: 5 additions & 5 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ protected function defineEnvironment($app): void
// load the engine defaults explicitly so tests mirror production.
$engineConfig = dirname(__DIR__) . '/vendor/laranail/env-kit/config/env-kit.php';
if (is_file($engineConfig)) {
$app['config']->set('env-kit', require $engineConfig);
$app['config']->set('laranail.env-kit', require $engineConfig);
}
}

Expand All @@ -48,10 +48,10 @@ protected function bindEnv(string $contents): string
file_put_contents($path, $contents);

config([
'env-kit.path' => $path,
'env-kit.backup_path' => $dir . '/backups',
'env-kit.audit.enabled' => false,
'env-kit.auto_backup' => false,
'laranail.env-kit.path' => $path,
'laranail.env-kit.backup_path' => $dir . '/backups',
'laranail.env-kit.audit.enabled' => false,
'laranail.env-kit.auto_backup' => false,
]);

$this->app->forgetInstance(EnvKitInterface::class);
Expand Down
Loading