# @mostajs/url — fiche LLM
> Helpers de construction et manipulation d'URL framework-agnostic, indispensables derrière un reverse proxy.

- Version: 0.5.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
- Chemin: mostajs/mosta-url · Statut audit: complet (dist/)

## RÔLE
Fournit des fonctions pures pour construire des URLs absolues, manipuler les query strings, générer des magic-links et signer/vérifier des URLs HMAC. Résout le problème du reverse proxy (Apache/nginx) où `req.url` côté Node donne le bind interne (`http://localhost:3020/...`) et non l'URL publique. Toute construction d'URL — callback OAuth, email, redirect, lien signé — doit s'appuyer sur une base configurée explicitement via un `BasePicker` global.

## INSTALLATION
npm i @mostajs/url

## EXPORTS
- Fonctions: `setBasePicker`, `absoluteUrl`, `baseFromHeaders`, `joinPath`, `isAbsolute`, `splitUrl`, `appendQuery`, `appendQueryParams`, `stripQuery`, `parseQuery`, `combineMagicLink`, `withNext`, `signedUrl`, `verifySignedUrl`
- Types: `AbsoluteUrlOptions`, `BasePicker`, `HeadersLike`, `MagicLinkOptions`, `SignedUrlOptions`, `VerifySignedUrlResult`
- Sous-chemin `./client`: composant React `CopyableLink`, type `CopyableLinkProps`

## EXPORTS PAR SOUS-CHEMIN
- `@mostajs/url` (`.`) — toutes les fonctions et types ci-dessus, framework-agnostic (Node + browser).
- `@mostajs/url/client` — `CopyableLink` : composant React (affichage compact d'URL + bouton copier clipboard, options `showOpen`, `showMailto`). React en peer dep optionnelle.

## API — SIGNATURES
- `setBasePicker(picker: BasePicker | null): void`
- `absoluteUrl(path: string, opts?: AbsoluteUrlOptions): string`
- `baseFromHeaders(headers: HeadersLike | Record<string,string|string[]|undefined>): string | null` — déduit `proto://host` PUBLIC depuis les headers de requête (`X-Forwarded-Host`/`Host` + `X-Forwarded-Proto`). Pour les hôtes dynamiques inconnus à l'avance : WebContainers (StackBlitz/Bolt) + reverse proxy. Retourne null si pas d'hôte. Complément runtime de `absoluteUrl` (base env-configurée).
- `joinPath(...parts: string[]): string`
- `isAbsolute(url: string): boolean` — true si protocole http(s)/data/file ou protocol-relative `//host`
- `splitUrl(url: string): { base: string; query: string; hash: string }`
- `appendQuery(url, key, value: string|number|boolean|null|undefined): string` — value null/undefined supprime le param
- `appendQueryParams(url, params: Record<string,...>): string` — null/undefined ignorés
- `stripQuery(url: string, key: string): string`
- `parseQuery(url: string): Record<string,string>` — premier seul, pas de multi-value
- `combineMagicLink(opts: MagicLinkOptions): string`
- `withNext(url: string, next: string|null|undefined): string` — `next` doit commencer par `/` sinon ignoré (anti open-redirect)
- `signedUrl(path: string, opts: SignedUrlOptions): string` — HMAC-SHA256 + expiry
- `verifySignedUrl(url: string, opts: Omit<SignedUrlOptions,'expiresInMs'|'params'>): VerifySignedUrlResult`

## TYPES CLÉS
- `AbsoluteUrlOptions { base?: string; fallback?: string }` — fallback default `'http://localhost:3000'`
- `BasePicker = () => string | null | undefined`
- `MagicLinkOptions { baseUrl: string; token: string; next?: string; extra?: Record<string,...> }`
- `SignedUrlOptions { secret: string; expiresInMs?: number; signatureParam?: string; expiryParam?: string; params?: Record<string,...> }` — expiresInMs default 1h, 0 = pas d'expiry ; signatureParam default `'sig'` ; expiryParam default `'exp'`
- `VerifySignedUrlResult = { valid: true; expiry: number|null } | { valid: false; reason: 'missing-signature'|'expired'|'bad-signature'|'invalid-expiry' }`

## PATTERN
```ts
import { absoluteUrl, setBasePicker, signedUrl, verifySignedUrl } from '@mostajs/url'
import { getEnv } from '@mostajs/config'

// Au démarrage : picker global lisant l'env
setBasePicker(() => getEnv('NEXTAUTH_URL') || getEnv('PUBLIC_URL'))

// Partout
absoluteUrl('/auth/callback')           // → 'https://app.example.com/auth/callback'

// URL signée
const url = signedUrl('/api/sfu/rooms/abc/whep', {
  secret: process.env.INVITE_SECRET!, expiresInMs: 60 * 60_000, params: { userId: '42' },
})
const r = verifySignedUrl(req.url, { secret: process.env.INVITE_SECRET! })
if (!r.valid) return new Response('Forbidden: ' + r.reason, { status: 403 })
```

## DÉPEND DE
Aucun module @mostajs/* requis. `@mostajs/config` est seulement suggéré dans les exemples pour alimenter le `BasePicker`. React en peer dep optionnelle (`./client` uniquement).

## PIÈGES
- Résolution de la base d'`absoluteUrl`, dans l'ordre : `opts.base` → retour du picker global → `opts.fallback` → `'http://localhost:3000'`.
- `setBasePicker` doit être appelé une seule fois au démarrage avant tout `absoluteUrl`.
- `withNext` rejette silencieusement tout `next` qui ne commence pas par `/` (protection open-redirect) — ne pas attendre une erreur.
- Dans `appendQuery`/`appendQueryParams`, `null`/`undefined` ne sont pas écrits ; `appendQuery` les utilise même pour SUPPRIMER un param existant, `appendQueryParams` les ignore simplement.
- `signedUrl`/`verifySignedUrl` : le `secret` ne doit jamais être commit ; passer via env.
- Pour un panneau QR complet, utiliser `@mostajs/qrpanel/client`, pas `CopyableLink`.

## RÉFÉRENCES
README.md
