# Vimine (browser)

> Riferimento della classe Vimine di @vimine/sdk — accesso, sessione, chiamate API, storage.

URL: https://developers.vimine.network/sdk/browser/

```js
import { Vimine } from '@vimine/sdk';
const vimine = new Vimine(options);
```

`Vimine` gestisce **Accedi con Vimine** (OAuth 2.1 + PKCE), la sessione del socio e le chiamate all'API
del socio. Un'istanza per client; creala una volta e riusala.

## Opzioni

| Opzione | Tipo | Default | |
|---|---|---|---|
| `clientId` | `string` | — | il tuo `vmc_…` (obbligatorio) |
| `redirectUri` | `string` | — | la pagina che chiama `handleRedirect()`, registrata per il client (obbligatorio) |
| `scopes` | `Scope[]` | `['profile:read', 'membership:read']` | permessi chiesti da `signIn()` |
| `storage` | `'session' \| 'local' \| 'memory' \| TokenStorage` | `'session'` | dove vivono sessione e accesso in corso |
| `storageKey` | `string` | `vimine:<clientId>:` | prefisso delle chiavi nello storage |
| `locale` | `Locale` | `'it'` | lingua delle pagine di accesso e consenso |
| `apiUrl` | `string` | `https://api.vimine.network` | origine dell'API |
| `appUrl` | `string` | `https://app.vimine.network` | origine delle pagine di accesso |
| `fetch` | `typeof fetch` | `globalThis.fetch` | per test e polyfill |
| `navigate` | `(url) => void` | `top.location.assign` | come lasciare la pagina |

Il costruttore lancia `VimineError` (`invalid_options`) senza `clientId` o `redirectUri`.

## Accesso

### `signIn(options?)`

Porta la **finestra principale** alla pagina di accesso e consenso. Chiamala da un gesto dell'utente (un
click). Da dentro un iframe di un'altra origine la navigazione è rifiutata (`framed`): la pagina di accesso non
si mostra mai in un frame.

| Opzione | |
|---|---|
| `scopes` | permessi di questo accesso |
| `prompt` | `'login'` (credenziali sempre) o `'consent'` (consenso sempre) |
| `returnTo` | uno stato tuo, conservato in locale e restituito da `handleRedirect()` — non viene inviato |

### `createSignInUrl(options?)`

Costruisce l'URL di autorizzazione e memorizza l'accesso in corso (verifier PKCE e `state`), senza navigare.
Utile se vuoi rendere tu il link.

### `hasRedirectParams(url?)`

`true` se l'URL (default: la pagina corrente) porta i parametri di un ritorno (`code`+`state` oppure `error`).

### `handleRedirect({ url?, cleanUrl? })`

Nella pagina di callback: verifica lo `state`, scambia il codice con i token e salva la sessione. Restituisce
`{ session, returnTo }`, oppure `null` se l'URL non ha i parametri di un accesso. Con `cleanUrl` (default
`true`) toglie `code`, `state` ed `error` dalla barra degli indirizzi.

Errori (`VimineError.code`): `access_denied` e gli altri errori OAuth se il socio non ha autorizzato;
`no_flow` se nessun accesso era stato avviato da questo browser (storage vuoto o scaduto); `state_mismatch` se
il ritorno non corrisponde; `invalid_grant` se il codice è scaduto o già usato.

## Sessione

### `getSession()` · `isSignedIn()`

La sessione salvata, o `null`.

```ts
interface Session {
    accessToken: string;
    refreshToken: string | null;
    expiresAt: number;            // ms, assoluto
    scope: Scope[];
    sub: string | null;           // identificativo del socio per la tua associazione (vms_…)
    mode?: 'live' | 'test';       // 'test' per le sessioni di un client di test
    aps?: AssociationRef | null;  // l'associazione: slug, name, picture_url
}
```

### `getAccessToken()`

Un access token valido, rinnovato se scade entro 30 secondi. Lancia `not_signed_in` senza sessione.

### `refresh()`

Ruota la sessione con il refresh token (una sola richiesta anche se chiamata più volte in parallelo). Se il
refresh token non è più utilizzabile la sessione viene cancellata e l'errore rilanciato.

### `signOut({ everywhere?, postLogoutRedirectUri?, state? })`

Revoca il refresh token (RFC 7009, best effort) e dimentica la sessione. Con `everywhere: true` porta poi il
browser a `/oauth/logout` dell'app Vimine, che chiude la sessione del socio e torna a
`postLogoutRedirectUri` (default `redirectUri`; deve stare su un'origine registrata).

### `onChange(listener)`

`listener(session | null)` a ogni accesso, rinnovo e uscita; con `storage: 'local'` anche per quelli avvenuti in
altre schede. Restituisce la funzione per disiscriversi.

## API del socio

### `request(path, init?)`

Una chiamata autenticata: `Authorization: Bearer`, JSON in entrata e in uscita, errori come `VimineError`,
un nuovo tentativo dopo il rinnovo se la risposta è `401`. `init.idempotencyKey` diventa l'header
`Idempotency-Key`.

```js
const events = await vimine.request('/v1/me/membership/events?limit=5');
```

### `me`

| | Scope | |
|---|---|---|
| `me.get()` | `profile:read` | il profilo (`Me`), con `codicefiscale_check`: come la persona è identificata (`null` = non ancora) |
| `me.update(patch)` | `profile:write` | telefono, genere, e l'identificazione: data, luogo e Stato di nascita (`birth_country`), codice fiscale — facoltativo, si può aggiungere più tardi. Un codice che non concorda risponde `422` con una voce `codicefiscale_mismatch`; se è `confirmable`, la persona lo conferma con `codicefiscale_confirmed: true` ([guida](https://developers.vimine.network/guides/members-without-cf/)) |

### `membership`

| | Scope | |
|---|---|---|
| `membership.get()` | `membership:read` | la tessera: `status`, `verdict`, `expires_at`, `statuto`, `card` |
| `membership.request(body, { idempotencyKey? })` | `membership:request` | richiesta di tesseramento; chiave di idempotenza generata se assente. `422 identification_required` se il profilo non è identificato: `err.errors` nomina ciò che manca, si completa con `me.update()` |
| `membership.events({ limit?, cursor? })` | `membership:read` | storico degli stati, paginato |
| `membership.statuto()` | `membership:read` | lo statuto in vigore e l'accettazione del socio |
| `membership.acceptStatuto({ version, sha256? })` | `statuto:accept` | registra l'accettazione |
| `membership.card()` | `membership:read` | il token della tessera (QR) e la sua finestra; `409 not_active` se l'adesione non è attiva |
| `membership.renew({ version, sha256? })` | `POST /v1/me/membership/renew` | rinnovo self-service: subito se gratuita senza approvazione, altrimenti richiesta alla segreteria |
| `membership.setNetworkOptIn(bool)` | `PATCH /v1/me/membership/network-opt-in` | consenso ai circuiti (`network:manage`) |
| `membership.googleWalletUrl()` | `GET /v1/me/membership/wallet/google` | link «Salva in Google Wallet»; `409 not_active` se l'adesione non è attiva |
| `membership.cardSvg()` | `GET /v1/me/membership/card.svg` | la tessera come SVG (con QR); `409 not_active` se l'adesione non è attiva |
| `membership.emailCard()` | `POST /v1/me/membership/card/email` | invia la tessera al socio per e-mail |

### `events`

Gli eventi pubblicati dall'associazione, compresi quelli riservati ai soci (`events:read`). Quelli pubblici
non richiedono accesso: `vimine.public.events`.

| | |
|---|---|
| `events.list({ filter?, from?, to?, limit? })` | `filter`: `upcoming` (predefinito), `past`, `cancelled`, `all` |
| `events.get(slug)` | copia, immagini, tipi di iscrizione con posti rimanenti |
| `events.eligibility(slug)` | il socio può iscriversi? per tipo, con le ragioni (`membership_required`, `sold_out`, …) |

### `registrations`

Le iscrizioni del socio (`registrations:read` / `registrations:write`). Vedi la guida [Eventi e iscrizioni](https://developers.vimine.network/guides/events/).

| | |
|---|---|
| `registrations.create(slug, { registration_type, note?, quantity? }, opts?)` | tipo gratuito → `confirmed` con il `token` del QR; con contributo → `reserved` con `payment_instructions`; `422 not_eligible` con le ragioni; `Idempotency-Key` generata |
| `registrations.list({ filter? })` · `registrations.get(id)` | le iscrizioni del socio (`filter: 'upcoming'`) |
| `registrations.cancel(id)` | annulla entro la finestra dell'evento (`409 window_closed` oltre) |
| `registrations.tokenPayload(id)` | il contenuto del QR di un'iscrizione confermata, o `null` |

### `connections`

| | |
|---|---|
| `connections.list()` | le app che il socio ha autorizzato in questa associazione |
| `connections.revoke(clientId)` | toglie il consenso a un'app (le sue sessioni terminano) |

### `public`

Gli endpoint pubblici, senza credenziali: vedi [`publicApi`](https://developers.vimine.network/sdk/public/).

## Storage

| `storage` | Dove | Quando |
|---|---|---|
| `'session'` (default) | `sessionStorage`: per scheda, sparisce chiudendola | il default più sicuro per un client pubblico |
| `'local'` | `localStorage`: condiviso tra schede, sopravvive al riavvio; l'uscita si propaga alle altre schede | app in cui il socio si aspetta di restare connesso |
| `'memory'` | solo in memoria | SSR, test, chioschi |
| `TokenStorage` | un tuo oggetto `{ get, set, remove }` | Node, storage cifrati, app native |

Dove lo storage del browser non è disponibile (modalità privata, `about:blank`) l'SDK ripiega in memoria.
`MemoryStorage` è esportata.

## Utilità

- `createPkce()` → `{ verifier, challenge, method: 'S256' }`, `createVerifier()`, `createChallenge(verifier)`,
  `createState()`, `base64url(bytes)` — PKCE e `state` su WebCrypto, se costruisci il flusso a mano.
- `DEFAULT_API_URL`, `DEFAULT_APP_URL`.
- `publicApi(options?)` — vedi [API pubblica](https://developers.vimine.network/sdk/public/).
- `VimineError` — vedi [Errori](https://developers.vimine.network/sdk/errors/).
