# Signed & Temporary URLs

Webrick signed URLs are generated by `Route` facade helpers and verified by `VerifySignedUrlMiddleware`.

Verification failures throw framework HTTP exceptions internally. The kernel's `ErrorHandler` converts them into the final `400`/`403`/`410` responses just before emission.

Current capabilities:

- plain named-route URLs via `Route::urlFor(...)`
- permanent signed URLs via `Route::signedUrlFor(...)`
- TTL-based temporary URLs via `Route::temporaryUrlFor(...)`
- explicit-expiry URLs via `Route::temporaryUrlUntil(...)`
- relative or absolute payload signing
- configurable signature and expiry parameter names
- configurable HMAC algorithm
- ignored query parameters during verification
- multiple verification keys for key rotation

## 1. Boot-time setup

```php
use Infocyph\Webrick\Middleware\VerifySignedUrlMiddleware;
use Infocyph\Webrick\Router\Definition\Registrar;
use Infocyph\Webrick\Router\Dispatch\MiddlewareAliases;
use Infocyph\Webrick\Router\Facade\Router as Route;
use Infocyph\Webrick\Router\Kernel\RouterKernel;
use Infocyph\Webrick\Router\Matching\ShardedMatcher;
use Infocyph\Webrick\Router\Route\Collection;
use Infocyph\Webrick\Router\Url\SignedUrlConfig;
use Psr\Log\NullLogger;

$signKey = $_ENV['WEBRICK_SIGN_KEY'] ?? 'change-me';
$baseUri = $_ENV['WEBRICK_URL_BASE_URI'] ?? 'http://localhost';
$signedUrls = new SignedUrlConfig(
    generationKey: $signKey,
    verificationKeys: [$signKey],
    defaultTtl: 900,
);

MiddlewareAliases::register(
    'verifySignedUrl',
    static fn() => new VerifySignedUrlMiddleware($signKey, 5),
);
MiddlewareAliases::register(
    'verifySignedUrlAbsolute',
    static fn() => new VerifySignedUrlMiddleware(new SignedUrlConfig(
        verificationKeys: [$signKey],
        payloadMode: SignedUrlConfig::MODE_ABSOLUTE,
        ignoredQueryParams: ['preview'],
        leeway: 5,
    )),
);

$kernel = RouterKernel::bootWithRegistrar(
    log: new NullLogger(),
    matcher: ShardedMatcher::make(),
    register: static function (Registrar $registrar): void {
        unset($registrar);
        require __DIR__ . '/../routes.php';
    },
    routeCache: __DIR__ . '/../.route-cache',
    registrarOptions: [
        'exposeUrlServices' => true,
        'signKey' => $signKey,
        'signedDefaultTtl' => 900,
        'signedUrlConfig' => $signedUrls,
        'urlBaseUri' => $baseUri,
    ],
    bindUrlServices: static function (Collection $routes) use ($signKey, $signedUrls, $baseUri): void {
        Route::bindUrlServices($routes, $signKey, 900, $signedUrls, $baseUri);
    },
    fallbackAliasesFromRegistrar: true,
);
```

If you do not want aliases, you can attach `new VerifySignedUrlMiddleware(...)` directly in a route's `middleware` option.

## 2. Define protected routes

```php
use Infocyph\Webrick\Request\Request;
use Infocyph\Webrick\Response\Response;
use Infocyph\Webrick\Router\Facade\Router as Route;

Route::get('/download/{file}', fn(string $file) => Response::attachment(__DIR__ . '/../files/' . $file, $file), [
    'as' => 'file.download',
    'middleware' => ['verifySignedUrl'],
]);

Route::get('/secure-absolute/{id:int}', fn(Request $request, string $id) => Response::json([
    'id' => (int) $id,
    'preview' => $request->getQueryParams()['preview'] ?? null,
]), [
    'as' => 'secure.absolute',
    'middleware' => ['verifySignedUrlAbsolute'],
]);
```

## 3. Generate URLs

```php
use Infocyph\Webrick\Router\Facade\Router as Route;
use Infocyph\Webrick\Router\Url\SignedUrlConfig;

$plain = Route::urlFor('file.download', ['file' => 'report.pdf']);
$signed = Route::signedUrlFor('file.download', ['file' => 'report.pdf']);
$temp = Route::temporaryUrlFor('file.download', ['file' => 'report.pdf'], ttl: 900);
$until = Route::temporaryUrlUntil('file.download', new DateTimeImmutable('+15 minutes'), ['file' => 'report.pdf']);

$absolute = Route::signedUrlFor('file.download', ['file' => 'report.pdf'], absolute: true);
$absolutePayload = Route::signedUrlFor(
    'secure.absolute',
    ['id' => 42],
    ['dl' => 1],
    absolute: true,
    payloadMode: SignedUrlConfig::MODE_ABSOLUTE,
);
```

## 4. Signed URL profiles with richer verification

```php
use Infocyph\Webrick\Router\Url\SignedUrlConfig;

$profile = new SignedUrlConfig(
    generationKey: 'active-signing-key',
    verificationKeys: ['active-signing-key', 'previous-signing-key'],
    defaultTtl: 900,
    signatureParam: '_sig',
    expiryParam: '_exp',
    algorithm: 'sha3-256',
    payloadMode: SignedUrlConfig::MODE_ABSOLUTE,
    ignoredQueryParams: ['preview', 'utm_source'],
    leeway: 10,
);
```

Use this when you need:

- key rotation
- absolute-payload verification
- tracking parameters that should not break signatures
- custom parameter names or algorithm choices

## 5. Operational notes

- Default reserved query params are `_sig` and `_exp`.
- Signed URL helpers reject attempts to inject reserved params from user-supplied query arrays.
- `temporaryUrlFor(...)` uses the helper TTL argument when provided, otherwise the configured default TTL.
- `temporaryUrlUntil(...)` is the better choice when expiration must align to a fixed timestamp.
- Absolute payload signing depends on a stable `urlBaseUri` and should be used intentionally.

## 6. Troubleshooting

### Missing signature

Usually the proxy stripped or failed to forward the query string.

**Nginx**

```nginx
location / {
    try_files $uri /index.php?$query_string;
}
```

**Apache**

```apache
RewriteRule ^ index.php [QSA,L]
```

### Invalid signature

Common causes:

- generation and verification use different keys
- query params were added or mutated after generation
- absolute payload mode was used during generation but not verification, or the reverse
- base URI or host does not match what the generator signed

### Expired URL

- TTL already elapsed
- explicit expiry timestamp is in the past
- clocks differ too much between generators and verifiers

Use `leeway` when you need a small clock-skew buffer.

## 7. Recommendations

- Use permanent signed URLs only for low-risk links.
- Use `temporaryUrlFor(...)` or `temporaryUrlUntil(...)` for downloads, callbacks, and sensitive actions.
- Prefer multi-key verification during signing-key rotations.
- If your application appends analytics params after generation, add them to `ignoredQueryParams` on the verifier profile.
