# samy.js, samy.css, Session-Token

Alles für die Seite deiner App: die Brücke, das Stylesheet und das Token.

Zum Nachschlagen für die Seite deiner [App](https://packlets.app/docs/apps.md): die Brücke `samy.js`, das Session-Token und das Stylesheet `samy.css`.

## samy.js

```html
<script src="https://packlets.app/embed/samy.js"></script>
```

Danach gibt es das Objekt `samy`:

| Aufruf | Ergebnis |
|---|---|
| `samy.inSamy` | `true`, wenn die Seite in der Samy-App läuft, sonst `false`. Kein Aufruf, sondern ein Wert. |
| `samy.launch` | [Startdaten](#startdaten), sofort beim Laden da: `{ v, packlet_id, platform, theme }`. Kein Aufruf, sondern ein Wert. |
| `samy.theme` | Samys Farben (siehe [Farben](#farben)). Kein Aufruf, sondern ein Wert. |
| `samy.ready()` | Meldet, dass die Seite fertig ist. Samy blendet dann den Platzhalter aus. Nur nötig mit `data-ready="manual"`. |
| `await samy.token()` | Das [Session-Token](#session-token) als Text. Direkt nach dem Laden kommt es ohne Wartezeit aus den Startdaten. |
| `await samy.fetch(url, init)` | Wie `fetch()`, setzt aber `Authorization: Bearer <Token>` mit einem frischen Token. Gibt die normale Antwort zurück. |
| `await samy.info()` | `{ app: "samy", version: 2, packlet_id: "…", platform: "iOS", theme: {…} }`. `platform` ist `iOS` oder `android`. |
| `samy.toast("Gespeichert")` | Kurze Meldung in der App. Der Text wird nach 140 Zeichen abgeschnitten. |
| `samy.open("https://…")` | Öffnet die Adresse im Browser des Handys. Nur `http(s)`. |
| `samy.haptic("success")` | Vibration: `tap` (Standard), `selection`, `success` oder `error`. |
| `await samy.storage.get(key)` | Gespeicherter Text oder `null`. Siehe [Speicher](#speicher). |
| `await samy.storage.set(key, text)` | Speichert Text (höchstens 16 KB). |
| `await samy.storage.remove(key)` | Löscht einen Eintrag. |
| `await samy.storage.keys()` | Alle Schlüssel dieser App. |

Alle Aufrufe außer `samy.inSamy` geben ein Promise zurück.

- **Außerhalb von Samy** (zum Beispiel im Browser) schlagen die Aufrufe sofort fehl. Frag deshalb zuerst `samy.inSamy` ab.
- **Zeitlimit:** Antwortet die App nicht innerhalb von 15 Sekunden, schlägt der Aufruf fehl.
- **Nur deine Adresse:** Antworten bekommen nur Seiten mit derselben Herkunft (`https`, Host und Port) wie die Adresse, die du als `page` gesetzt hast.
- **Nur die Hauptseite:** Jeder Aufruf trägt einen geheimen Brücken-Schlüssel aus den [Startdaten](#startdaten). Aufrufe aus iframes fremder Adressen haben ihn nicht, Samy ignoriert sie. Mehr unter [iframes und fremde Inhalte](https://packlets.app/docs/seiten.md#5-iframes-und-fremde-inhalte).
- Läuft die Seite in Samy, trägt `<html>` die Klasse `in-samy`. Ist das Skript geladen, feuert auf `window` das Ereignis `samy:ready`.
- **Wieder geöffnet:** Samy hält die zuletzt geöffneten Seiten im Speicher (bis zu vier). Kommt jemand zurück, lädt die Seite nicht neu. Stattdessen feuert auf `window` das Ereignis `samy:activated`. Das ist der richtige Moment, veraltete Daten aufzufrischen. Der Knopf oben rechts in der App lädt die Seite bei Bedarf ganz neu.

### Laden und Platzhalter

Bis deine Seite fertig ist, zeigt Samy Avatar und Name der App. Normalerweise verschwindet der Platzhalter, sobald die Seite geladen ist. Lädt deine Seite danach noch Daten nach, bindest du das Skript mit `data-ready="manual"` ein und rufst selbst `samy.ready()`, wenn alles steht. Kommt nach sechs Sekunden kein `ready()`, blendet Samy den Platzhalter trotzdem aus.

```html
<script src="https://packlets.app/embed/samy.js" data-ready="manual"></script>
<script>
  loadEverything().then(() => samy.ready());
</script>
```

### Startdaten

Samy hängt beim Laden Startdaten an die Adresse, hinter `#samy=` (Base64url-kodiertes JSON). Der Teil hinter `#` geht nie an deinen Server. `samy.js` liest ihn sofort aus, stellt ihn als `samy.launch` bereit und entfernt ihn wieder aus der Adresse. Das Session-Token aus den Startdaten liefert `samy.token()` in den ersten 45 Sekunden ohne Umweg über die App. So weiß deine Seite schon beim ersten Aufruf deines Servers, wer sie öffnet (wie `initData` bei Telegram).

Den Brücken-Schlüssel aus den Startdaten legt `samy.js` im `sessionStorage` deiner Adresse ab. So funktioniert `samy` auch auf weiteren Seiten, zu denen du innerhalb deiner Adresse verlinkst. Du musst dafür nichts tun.

### Farben

`samy.theme` enthält Samys Farben: `bg`, `surface`, `text`, `text_secondary`, `text_tertiary`, `field`, `cta`, `on_cta`, `danger` und `scheme` (`light`). `samy.js` setzt sie zusätzlich als CSS-Variablen auf `<html>`, zum Beispiel `--samy-bg`, `--samy-text`, `--samy-cta` oder `--samy-text-secondary`. Damit passt deine Seite ohne eigene Farbwerte zur App.

### Abstände

In Samy füllt deine Seite die ganze Fläche unter Samys schmaler Kopfzeile bis zum unteren Rand (Vollbild). Die Kopfzeile bleibt immer gleich und verdeckt nichts. Unten setzt Samy die CSS-Variable `--samy-inset-bottom` (Home-Leiste des iPhones) und gibt `<html>` diesen Abstand als Innenabstand; `--samy-inset-top` gibt es weiter, ist aber `0`. Normalerweise musst du nichts tun.

- Willst du die volle Höhe ohne Scrollen füllen, rechne die Abstände heraus: `min-height: calc(100vh - var(--samy-inset-top) - var(--samy-inset-bottom))`.
- Willst du alles selbst regeln, schreib `<html data-samy-insets="manual">` und nutz die Variablen an passender Stelle.

### Speicher

`samy.storage` speichert kleine Texte auf dem Gerät, getrennt nach Konto und App. Teilen sich zwei Konten ein Handy, sieht keins die Einträge des anderen. Das unterscheidet ihn vom normalen `localStorage` der Seite. Bis zu 100 Einträge je App, je höchstens 16 KB. Für Objekte nimm `JSON.stringify` und `JSON.parse`. Meldet sich jemand ab, bleiben die Einträge auf dem Gerät. Löscht er das Konto, sind sie wertlos, weil niemand sonst sie lesen kann.

## Session-Token

Ein JWT, das sagt, wer die Seite gerade in Samy öffnet.

| | |
|---|---|
| Algorithmus | `HS256` |
| Geheimnis | SHA-256-Hash deines Schlüssels als Hex-Text in Kleinbuchstaben. Dasselbe wie bei der [Webhook-Signatur](https://packlets.app/docs/ereignisse.md#signatur). |
| Gültigkeit | 60 Sekunden |

| Feld | Inhalt |
|---|---|
| `iss` | `https://api.packlets.app` |
| `aud` | ID deines Packlets. Dieselbe wie `packlet_id` in jedem Webhook-Ereignis. |
| `sub` | ID des Nutzers in Samy. Bleibt für denselben Nutzer immer gleich. |
| `email` | E-Mail-Adresse des Nutzers. |
| `name` | Name des Nutzers, kann fehlen. |
| `iat`, `exp` | Ausgestellt und gültig bis, als Unix-Zeit. |
| `jti` | Eindeutige Kennung dieses Tokens. |

Prüf auf deinem Server immer drei Dinge: die Signatur, `exp` und dass `aud` dein Packlet ist. Erzeugst du in der App einen neuen Schlüssel, sind alle Tokens sofort ungültig.

## Token von Samy prüfen lassen

Ohne eigene JWT-Bibliothek fragt dein Server Samy. Dieser Aufruf braucht keinen Schlüssel.

```http
POST https://api.packlets.app/v1/session-token/verify
Content-Type: application/json
```

| Feld | Typ | Pflicht | Bedeutung |
|---|---|---|---|
| `token` | Text | ja | Das Token aus dem Header `Authorization` der Anfrage an deinen Server. |

Antwort bei einem gültigen Token:

```json
{
  "valid": true,
  "packlet_id": "9d2f7c10-3a5b-4c8e-9f21-6b7d8e9f0a12",
  "page_url": "https://deine-seite.example/app",
  "user": { "id": "4f1c…", "email": "du@example.com", "name": "Du" },
  "expires_at": "2026-11-10T08:16:00.000Z"
}
```

Sonst Status `401`:

```json
{ "valid": false, "error": "invalid_token", "message": "Das Token ist ungültig oder abgelaufen." }
```

> **Achtung:** Vergleich `packlet_id` mit der ID deines Packlets. Sonst nimmt dein Server auch Tokens an, die für ein fremdes Packlet ausgestellt wurden. Bedient dein Server viele Packlets (zum Beispiel eine App für alle Nutzer), prüf stattdessen `page_url`: Das ist die Seite, die das Packlet gerade zeigt.

Für den Aufruf gilt das [Rate-Limit](https://packlets.app/docs/api.md#rate-limit), gezählt je Adresse deines Servers.

## samy.css

```html
<link rel="stylesheet" href="https://packlets.app/embed/samy.css">
```

Das Stylesheet bringt die Schrift Lato, die Farben der App und diese Bausteine:

| Klasse | Wofür |
|---|---|
| `samy` | Rahmen der Seite: 16 Pixel Innenabstand, Bausteine untereinander mit 12 Pixel Abstand, mindestens so hoch wie der Bildschirm. |
| `samy-title`, `samy-big` | Überschrift (20 px) und große Zahl (44 px). |
| `samy-sub`, `samy-label`, `samy-note` | Grauer Nebentext, Abschnitts-Label in Großbuchstaben, kleiner Hinweis. |
| `samy-row`, `samy-spread`, `samy-grow`, `samy-push` | Nebeneinander; an die Ränder verteilen; Platz füllen; nach unten schieben. |
| `samy-button` | Knopf wie in der App, 44 px hoch. Dazu `is-secondary` (hell) und `is-small` (32 px). |
| `samy-tabs` | Umschalter zwischen Ansichten. Der aktive Tab trägt `aria-selected="true"`. |
| `samy-list` | Liste mit feinen Trennlinien, Zeilen mindestens 44 px hoch. |
| `samy-table` | Tabelle. Zahlenspalten mit `is-num` rechtsbündig. |
| `samy-field` | Eingabefeld. |
| `samy-chip`, `samy-dot` | Kleines Kennzeichen; Status-Punkt grün, mit `is-warn` rot. |
| `samy-bars` | Kleines Balkendiagramm aus `<i style="height: 40%">`. Der aktive Balken trägt `is-active`. |

Farben und Maße als Variablen:

| Variable | Bedeutung |
|---|---|
| `--samy-bg` | Hintergrund |
| `--samy-text`, `--samy-text-2`, `--samy-text-3` | Haupttext, Nebentext, noch hellerer Text |
| `--samy-icon` | Symbole |
| `--samy-cta`, `--samy-on-cta` | Dunkelblau der Knöpfe und die Schrift darauf |
| `--samy-field` | Hellgrau für Felder und helle Knöpfe |
| `--samy-selected`, `--samy-bar` | Ausgewählter Tab, Balken im Diagramm |
| `--samy-divider` | Trennlinien |
| `--samy-ok`, `--samy-warn` | Grün und Rot für Status |
| `--samy-radius` | Eckenradius |

Text lässt sich auf der Seite nicht markieren (`user-select: none`), wie in einer App. Eine fertige Beispielseite liegt unter [/embed/beispiel.html](https://packlets.app/embed/beispiel.html).
