# Prüfplan — Shopify-Seite

Was hier steht, liess sich **ohne einen echten Shopify-Shop nicht klären**.
Alles, was sich an der Dokumentation klären liess, ist aus dieser Liste
entfernt und steht am Ende unter «Was bereits belegt ist» — damit niemand
dieselbe Arbeit zweimal macht.

Sortiert nach Risiko: Wer oben abbricht, braucht unten nicht weiterzulesen.

Es geht durchgehend um den **Shop-seitigen** Teil: `shopify.app.toml`, Theme
App Extension, Validation Function. Der Server (App Proxy, Webhook,
Admin-API-Aufrufe) hat seinen eigenen Plan.

> **Zur Altersgrenze steht hier nichts zu prüfen — und das ist der Befund.**
> Die Schnittstelle kann gegen 16 oder gegen 18 prüfen, und die anderen drei
> Pakete stellen das je Warengruppe ein. Dieses Paket nicht: Shopifys
> Warengruppe ist die Sammlung, und der App-Block kennt genau eine. Es gilt
> deshalb **eine** Grenze für alles, und sie kommt aus der Vorgabe des
> EdelVerify-Kontos — dieselbe, gegen die der App-Proxy den Nachweis wieder
> einlöst.
>
> Der Block schickt bewusst **kein** `min_age` mit. Täte er es, könnte die
> Grenze im Theme von der Vorgabe abweichen: Der Laden startete eine
> 16er-Prüfung, der Server verlangte 18, und die Kundschaft sähe ein grünes
> Prüffenster und danach eine abgewiesene Rückmeldung. Nachgeprüft wird das
> statisch: `python3 scripts/pakete-pruefen.py`, Abschnitt K — «setzt die
> Grenze nirgends selbst».
>
> Die Einschränkung steht ausführlich in `EINBAU.md`, Abschnitt 0. Sie gehört
> ins Verkaufsgespräch, nicht in eine Fussnote nach der Installation.

> **Es gibt keinen Installationsweg — und auch das ist ein Befund, kein
> Prüfpunkt.** Der OAuth-Weg ist nicht gebaut: Die Rückrufadressen, die Shopify
> nach der Zustimmung anspringen würde, antworten auf `verify.edelbyte.ch` mit
> **404** (gemessen für `/auth/callback` und `/auth/shopify/callback`). Ein
> eingebetteter App-Bereich im Shopify-Admin ist ebenfalls ausgeschlossen: Die
> Seiten liefern `X-Frame-Options: DENY` und `frame-ancestors 'none'`, ein
> Rahmen kann dort also nie rendern.
>
> Solange das so ist, legt EdelByte jeden Shop **von Hand** an, und alle
> Einstellungen aus `EINBAU.md`, Schritt 4, nehmen wir vor. Für diesen Prüfplan
> heisst das: Die Prüfungen unten setzen einen bereits von Hand angelegten
> Entwicklungsshop voraus — sie prüfen den Shop-seitigen Teil, nicht die
> Installation. Wer den OAuth-Weg baut, braucht dafür einen eigenen Plan; die
> Prüfungen hier decken ihn nicht ab.

---

## 0. Umgebung

```bash
npm install -g @shopify/cli@latest
shopify version

# Im Ordner der App:
shopify app config link          # verbindet mit der App im Dev Dashboard
shopify app dev --store=<dev-store>.myshopify.com
```

Ausserdem gebraucht:

- ein **Entwicklungsshop** mit einem Online-Store-2.0-Theme (Dawn genügt) und
  einer Sammlung, die im Vertriebskanal «Online Store» verfügbar ist
- ein EdelVerify-Testkonto mit `pk_test_…` / `sk_test_…`; die Domain des
  Entwicklungsshops muss dort als **erlaubte Herkunft** eingetragen sein,
  sonst scheitert Prüfung 6 und man sucht den Fehler im falschen System
- ein Handy mit der swiyu-App und einer Beta-E-ID
- für Weg B: ein Shop **mit Shopify Plus**. Ein gewöhnlicher Entwicklungsshop
  genügt nicht — Functions in Custom-Apps sind Plus vorbehalten. Ob ein
  Partner-Development-Store die Plus-Merkmale mitbringt, ist selbst zu prüfen
  (Prüfung 9).

---

## Hoch

### 1. Überlebt ein Warenkorb-Attribut mit führendem Unterstrich die Bestellung?

Die ganze Bindung des Nachweises an die Bestellung hängt daran. Belegt ist nur,
dass **doppelter** Unterstrich das Attribut überall verschwinden lässt; über
den einfachen sagt die Dokumentation nichts.

**Erste Bauhandlung, vor allem anderen.** Im Laden, in der Browserkonsole:

```js
await fetch(window.Shopify.routes.root + 'cart/update.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    attributes: { _edelverify: 'mit-strich', edelverify: 'ohne-strich' },
  }),
}).then((r) => r.json())
```

Dann eine Testbestellung abschliessen und nachsehen:

```bash
# Nutzdaten des Webhooks (Server-Log) — Feld note_attributes
# und/oder über die Admin API:
shopify app function replay   # nein, hier nicht; stattdessen:
```

```graphql
query {
  orders(first: 1, reverse: true) {
    nodes { name customAttributes { key value } }
  }
}
```

**Erwartet:** beide Schlüssel kommen an.
**Wenn nur `edelverify` ankommt:** an drei Stellen umstellen und nirgends sonst —
`blocks/edelverify.liquid` (`ev_attribut`),
`src/cart_validations_generate_run.graphql` (`attribute(key:)`) und die
Webhook-Auswertung im Server.

### 2. Sieht die Validation Function dasselbe Attribut? *(nur mit Plus)*

Auch wenn das Attribut in der Bestellung ankommt, ist damit nicht gesagt, dass
`cart.attribute(key: "_edelverify")` in der Function-Eingabe etwas liefert.

```bash
shopify app function run          # mit einer eigenen Eingabe-JSON
shopify app function replay       # mit einer echten Ausführung aus dem Shop
```

**Erwartet:** In der Eingabe der abgespielten Ausführung steht
`"attribute": { "value": "…" }`. Steht dort `null`, obwohl das Attribut im
Warenkorb liegt, ist die Function auf Plus wirkungslos — dann bleibt nur der
Webhook, und das gehört dem Kunden gesagt.

### 3. Übersetzt die Function überhaupt?

Der Rust-Teil ist ein **Gerüst**. Die erzeugten Typpfade
(`schema::cart_validations_generate_run::input::cart::lines::Merchandise`) und
die Rückgabearten der Zugriffsmethoden (`&String` gegen `&str`, `&bool` gegen
`bool`) stammen aus der Dokumentation, nicht aus einem Übersetzungslauf.

```bash
cd extensions/edelverify-gate
shopify app function schema          # holt schema.graphql
rustup target add wasm32-unknown-unknown
cargo build --target=wasm32-unknown-unknown --release
cargo doc --open                     # zeigt die tatsächlich erzeugten Typen
ls -l target/wasm32-unknown-unknown/release/edelverify_gate.wasm
```

**Erwartet:** Übersetzung ohne Fehler, Datei **unter 256 kB**. Weicht ein
Typpfad ab, steht die richtige Schreibweise in `cargo doc`; die Logik bleibt
davon unberührt.

Ebenfalls hier zu klären: ob die erzeugte Wasm-Datei bei einem Paketnamen mit
Unterstrich tatsächlich `edelverify_gate.wasm` heisst — der Pfad in
`shopify.extension.toml` muss stimmen, sonst schlägt der Deploy fehl.

### 4. Der Cart-Token in der Rückmeldeadresse

Unser Skript hängt den Token aus `GET /cart.js` als `?ct=` an die
Proxy-Adresse. Zwei Fragen auf einmal:

1. Reicht der App Proxy eigene Abfrageparameter durch und nimmt sie in die
   Signatur auf? (Das Beispiel in der Dokumentation zeigt `extra=1&extra=2` —
   aber unser Wert enthält selbst ein `?key=…`, also kodierte Sonderzeichen.)
2. Ist der Wert derselbe wie `cart_token` in den Nutzdaten von
   `orders/create`, oder nur der Teil vor `?key=`?

**Handgriff:** Im Laden eine Prüfung durchführen, im Server-Log die
ankommende URL und den berechneten Signaturvergleich mitschreiben; danach die
Bestellung abschliessen und `cart_token` aus dem Webhook danebenlegen.

**Erwartet:** Signatur stimmt, und die beiden Token lassen sich aufeinander
abbilden. Wenn nicht, wird serverseitig auf den Teil vor `?key=` normalisiert —
an **einer** Stelle, nicht an zweien.

### 5. Wird `gate.js` von einer Content-Security-Policy geblockt?

Das Skript lädt `https://verify.edelbyte.ch/v1/gate.js` **dynamisch** nach.
Setzt das Theme (oder eine andere App) eine CSP mit `script-src`, ohne unsere
Domain zu nennen, passiert nichts — und zwar lautlos ausser einer Meldung in
der Konsole.

**Handgriff:** Warenkorbseite öffnen, Konsole beobachten, dann auf «Alter
bestätigen» klicken.

**Erwartet:** Das Skript lädt, `window.EdelVerify` existiert, das Fenster geht
auf. Bei einer CSP-Meldung: mit dem Theme-Anbieter klären, ob
`verify.edelbyte.ch` in `script-src` und `connect-src` aufgenommen werden kann.

---

## Mittel

### 6. Die Prüfung von Anfang bis Ende, mit echter E-ID

- [ ] Streifen erscheint, sobald ein Artikel der Sammlung im Warenkorb liegt
- [ ] Auf der Warenkorbseite legt sich die Ebene darüber; Escape schliesst sie
- [ ] «Alter bestätigen» öffnet das Fenster von `gate.js`, QR erscheint
- [ ] Mit swiyu bestätigen → Fenster wird grün
- [ ] Im Netzwerk-Tab: `POST /apps/edelverify/confirm?ct=…` antwortet
      `200 {"ok":true}` **vor** dem Neuladen
- [ ] Danach `POST /cart/update.js` mit dem Attribut, **dann** Neuladen (~0,9 s)
- [ ] Nach dem Neuladen: kein Streifen mehr, Kassenknöpfe frei

Die Reihenfolge der letzten drei Punkte **ist** der Test. Lädt die Seite neu,
bevor `/confirm` geantwortet hat, wird die Übermittlung abgebrochen und der
Nachweis ist verbrannt — er wird genau einmal ausgeliefert. Deshalb hängt das
Neuladen an `edelverify:stored`, nicht an `edelverify:verified`.

Danach ins Server-Log sehen: **weder `sk_…` noch `av_…` darf dort auftauchen.**

### 7. Überschreibt `cart/update.js` fremde Attribute?

Manche Themes und Apps schreiben selbst Warenkorb-Attribute. Ob unser Aufruf
nur den eigenen Schlüssel setzt oder die übrigen mitnimmt, ist zu messen —
sonst löschen wir einem anderen etwas weg (oder umgekehrt, und unser Nachweis
verschwindet beim nächsten Klick des Themes).

```js
await fetch(window.Shopify.routes.root + 'cart/update.js', {
  method: 'POST', headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ attributes: { fremd: 'bleibt?' } }),
}).then(r => r.json())
// dann unser Attribut setzen und erneut GET /cart.js lesen
```

**Erwartet:** `fremd` steht danach noch da.

### 8. Kassenknöpfe in echten Themes

Die Auswahl `[name="checkout"], .shopify-payment-button__button,
form[action*="/checkout"] [type="submit"]` deckt Dawn ab. Ein gekauftes Theme
kann andere Knöpfe haben, vor allem in der Warenkorb-Schublade.

- [ ] Dawn: Warenkorbseite und Schublade
- [ ] **Das Theme des Kunden**, beide Orte
- [ ] Nach dem Öffnen/Schliessen der Schublade greift die Sperre weiterhin
      (Beobachter)
- [ ] Express-Checkouts (Shop Pay, Apple Pay, Google Pay) gehen daran vorbei —
      das ist erwartet und ohne Plus nicht zu ändern. Trotzdem einmal ansehen,
      damit es niemanden überrascht.

### 9. Läuft die Function auf dem Zielsystem überhaupt? *(nur mit Plus)*

Zu klären, bevor irgendetwas zugesagt wird:

- Tarif je Shop des Kunden (nur Plus kann Custom-Apps mit Functions)
- ob die Shops in **einer** Plus-Organisation liegen (entscheidet, ob eine
  Installation genügt)
- ob ein Partner-Development-Store die Function annimmt (zum Entwickeln)

```bash
shopify app deploy
# danach im Admin: Einstellungen → Checkout → Validierungen
```

**Erwartet:** Die Validierung erscheint und lässt sich einschalten. Kommt eine
Tarifmeldung, ist Weg B für diesen Shop erledigt — dann gilt Weg A, und das
gehört ins Kundengespräch, nicht in ein Ticket.

### 10. Instruktionsbudget

Erst messen, dann die Härtung aus Bauvorgabe 6.5 (signiertes Attribut mit
HMAC-SHA256) zusagen.

```bash
shopify app function replay
```

**Erwartet:** Die einfache Fassung liegt weit unter den Referenzbeispielen
(17 177 bzw. 19 272 Instruktionen für vergleichbare Prüfungen). Erst wenn
HMAC-SHA256 dazukommt, wird es eng — dann hier nachmessen, nicht schätzen.

### 11. Der Umfang der Function

Die Collection-IDs stehen als Literal in der Eingabeabfrage und werden beim
Deploy eingesetzt; das Metafeld `edelverify/config` entscheidet nur zwischen
«ganzer Shop» und «nur diese Collection».

- [ ] Metafeld gesetzt (`metafieldsSet` auf die `Validation`) → Function sieht
      den Wert in der Eingabe
- [ ] Metafeld **nicht** gesetzt → jeder Warenkorb gilt als altersbeschränkt
      (gewollt: fail-closed), und der Händler merkt es sofort
- [ ] Platzhalter `COLLECTION_ID_EINSETZEN` nicht ersetzt → dieselbe Wirkung
      wie ohne Metafeld, nie eine stille Freigabe

---

## Niedrig

### 12. Theme-Editor: Block, Einstellungen, Übersetzungen

- [ ] **Altersprüfung (E-ID)** erscheint unter Theme-Einstellungen →
      App-Einbettungen
- [ ] Alle sechs Einstellungen sind da, mit deutschen Beschriftungen — steht
      dort roher Text wie `t:settings.public_key.label`, findet Shopify die
      Schema-Sprachdateien nicht
- [ ] Der Direktlink zum Einschalten funktioniert:
      `https://<shop>.myshopify.com/admin/themes/current/editor?context=apps&template=index&activateAppId=<client_id>/edelverify`
      (der letzte Teil ist der **Dateiname** des Blocks, nicht der Name der
      Extension)
- [ ] Im Editor erscheint der Streifen, ohne dass etwas gesperrt wird
      (`Shopify.designMode`)

### 13. Sprachwahl im Laden

- [ ] Ein deutschsprachiger Laden zeigt die Texte aus `de.json`
- [ ] fr / it ebenso
- [ ] Eine nicht abgedeckte Sprache fällt auf `en.default.json` zurück und
      zeigt Englisch — keine leeren Streifen, keine Schlüsselnamen

Offen ist dabei, ob eine Theme App Extension zwingend `en.default.json` als
Vorgabedatei verlangt oder auch `de.default.json` annähme. Deshalb liegt
Englisch als Vorgabe bei; das ist die risikoärmere Wahl, nicht die schönste.

### 14. Auswahlfeld statt Handle

Die Sammlung wird heute als **Text** (Handle) eingetragen. Schöner wäre ein
Auswahlfeld (`"type": "collection"`). Belegt ist nur, dass Resource-Settings in
**App blocks** existieren; für **App embed blocks** sagt die Dokumentation
nichts, und sie können ausdrücklich nicht auf dynamische Quellen zeigen.

**Handgriff:** In einer Wegwerf-Fassung des Blocks `"type": "collection"`
setzen, `shopify app deploy`, im Editor nachsehen. Wird das Feld angeboten und
liefert `block.settings.collection.handle` einen Wert, wird umgestellt — das
spart die häufigste Fehleingabe (Titel statt Handle).

### 15. Warenkorb-Schublade und `request.page_type`

Die sperrende Ebene erscheint nur, wenn `request.page_type == 'cart'`. Themes
mit Schublade führen den Kunden nie auf diese Seite.

- [ ] In einem Schubladen-Theme: Klick auf «Zur Kasse» in der Schublade wird
      abgefangen und öffnet die Prüfung direkt
- [ ] Nach dem Abbrechen bleibt der Kunde in der Schublade, nichts hängt

### 16. Grössengrenzen bei Deploy

```bash
shopify app deploy
```

Shopify prüft die Extension beim Deploy: alle Dateien zusammen ≤ 10 MB,
höchstens 30 Blöcke, Liquid über alle Dateien ≤ 100 kB, je Sprachdatei ≤ 15 kB.
Für JavaScript und CSS gelten 10 kB bzw. 100 kB als **Empfehlung**, nicht als
harte Grenze. Unsere Dateien liegen deutlich darunter — nachsehen lohnt erst,
wenn jemand etwas hinzufügt.

---

# Was bereits belegt ist

Diese Punkte standen zur Frage und sind an shopify.dev geklärt (Stand
14. August 2026). Sie gehören nicht mehr geprüft.

| Frage | Antwort | Quelle |
|---|---|---|
| Aufbau von `shopify.app.toml` | `name`, `client_id`, `application_url`, `embedded`, `handle`, `[access_scopes] scopes`, `[auth] redirect_urls`, `[webhooks] api_version` + `[[webhooks.subscriptions]]` mit `topics` / `compliance_topics` / `uri`, `[app_proxy] url/prefix/subpath`, `[build]` | [app-configuration](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration) |
| Erlaubte Werte für `prefix` | `a`, `apps`, `community`, `tools`; `subpath` ≤ 30 Zeichen, nicht `admin`, `services`, `password`, `login` | ebenda |
| Inhalt von `shopify.extension.toml` (Theme) | nur `name`, `type = "theme"`, optional `handle` | [theme-app-extensions/configuration](https://shopify.dev/docs/apps/build/online-store/theme-app-extensions/configuration) |
| Ordner der Theme App Extension | `assets`, `blocks`, `snippets`, `locales`, `package.json`, `shopify.extension.toml` | ebenda |
| App embed block | `"target"` ist `head`, `compliance_head` oder `body`; wird vor `</head>` bzw. `</body>` eingefügt; **ab Werk deaktiviert**, der Händler muss ihn im Theme-Editor einschalten | ebenda |
| Weitere Schema-Attribute | `name` (< 25 Zeichen, ohne App-Namen), `stylesheet`, `javascript`, `enabled_on`/`disabled_on`, `class`, `tag`, `settings`, `default`, `available_if` | ebenda |
| Blöcke im Checkout | «App blocks and app embed blocks can't be rendered on checkout pages» — deshalb liegt die sperrende Ebene über dem **Warenkorb** | ebenda |
| Grenzen | 10 MB gesamt, 30 Blöcke, 100 Sprachdateien à 15 kB, Liquid ≤ 100 kB (erzwungen); CSS 100 kB / JS 10 kB (empfohlen) | ebenda |
| Direktlink zum Einschalten | `…/admin/themes/current/editor?context=apps&template={template}&activateAppId={api_key}/{handle}`, `{handle}` = Dateiname des Blocks | ebenda |
| Privates Warenkorb-Attribut | **doppelter** Unterstrich; solche Attribute sind «not available in the Liquid `cart.attributes` object or the Ajax API» — für uns unbrauchbar | [Ajax Cart API](https://shopify.dev/docs/api/ajax/reference/cart) |
| Felder aus `GET /cart.js` | `token`, `item_count`, `attributes`, `items[].key`, `.product_id`, `.variant_id`, `.quantity`; Token in der Form `Z2Nw…?key=…` | ebenda |
| Sprachbewusste Ajax-Adressen | `window.Shopify.routes.root`, endet immer auf `/` | [Ajax API](https://shopify.dev/docs/api/ajax) |
| `product.collections` in Liquid | vorhanden — **aber** «Collections that aren't available on the Online Store sales channel aren't included». Daher der Pflichtschritt im EINBAU. | [product](https://shopify.dev/docs/api/liquid/objects/product) |
| `request.page_type` | Werte u. a. `cart`, `product`, `collection`, `index` | [request](https://shopify.dev/docs/api/liquid/objects/request) |
| App-Proxy-Signatur | Parameter `shop`, `path_prefix`, `timestamp`, `logged_in_customer_id`, `signature`; unkodiert, sortiert, verkettet, HMAC-SHA256 mit dem Client Secret, hex | [authenticate-app-proxies](https://shopify.dev/docs/apps/build/online-store/app-proxies/authenticate-app-proxies) |
| Cookies über den Proxy | keine — Shopify entfernt `Cookie` und `Set-Cookie` | ebenda |
| Händler ändert die Proxy-URL | Einstellungen → Apps und Vertriebskanäle → {App} → App-Proxy → URL anpassen; Änderungen an `subpath`/`prefix` in der App wirken nur auf **neue** Installationen | [app-proxies](https://shopify.dev/docs/apps/build/online-store/app-proxies) |
| Function: Netzzugriff | für gewöhnliche Plus-Händler nicht verfügbar | [network-access](https://shopify.dev/docs/apps/build/functions/input-output/network-access) |
| Function: `inAnyCollection(ids:)` | vorhanden, `ids` ist Pflicht und ein **Literal** — Eingabeabfragen kennen keine Laufzeitvariablen | [Cart and Checkout Validation](https://shopify.dev/docs/api/functions/latest/cart-and-checkout-validation) |
| Function: `cart.attribute(key:)` | vorhanden, liefert `key` und `value` | ebenda |
| Function: `validation.metafield(namespace:key:)` | vorhanden, `value` ist immer eine Zeichenkette | ebenda |
| Function: Ziel der Fehlermeldung | «You can also use `$.cart` to target the current checkout page» | ebenda |
| Function in Rust | `#[typegen("schema.graphql")]` mit `#[query("src/….graphql")]`, Einstieg `#[shopify_function] fn cart_validations_generate_run(input: schema::cart_validations_generate_run::Input)`; Union über `schema::…::input::cart::lines::Merchandise::ProductVariant(v)`; `main()` bricht ab | [Rust for Functions](https://shopify.dev/docs/apps/build/functions/programming-languages/rust-for-functions), Beispiele in der Validation-Referenz |
| Function: Bauziel | ab `shopify_function` 2.0 `wasm32-unknown-unknown`, Wasm-Datei < 256 kB | [Rust for Functions](https://shopify.dev/docs/apps/build/functions/programming-languages/rust-for-functions) |
| Functions und Tarif | «Only stores on a Shopify Plus plan can use custom apps that contain Shopify Function APIs.» | [functions](https://shopify.dev/docs/apps/build/functions) |
| Kann der Laden die Altersgrenze selbst wählen? | nein, und zwar absichtlich | Die Sammlung ist die Warengruppe; es gibt genau eine. Die Grenze kommt aus der Vorgabe des Händlerkontos, gegen die auch `/api/shopify/proxy/confirm` einlöst. Belegt durch `scripts/pakete-pruefen.py`, Abschnitt K. |
| Zeigt das Prüffenster die richtige Grenze? | ja | `gate.js` liest `min_age` aus der Antwort des Dienstes und schreibt sie in den Text («Über 16 bestätigt»). Es rät sie nicht. Nachgemessen: `scripts/api-vertragstest.py`, Abschnitt 10. |
| Woran erkennt der Erfolg sich — `over_18` oder `age_ok`? | `age_ok` | `over_18` ist nur wahr, wenn gegen 18 geprüft wurde; bei einer bestandenen 16er-Prüfung steht dort `false`. Sowohl `gate.js` als auch `/api/shopify/proxy/confirm` lesen deshalb `age_ok`. In einem Laden mit Grenze 16 wäre die andere Wahl eine Kundschaft, die alt genug ist und trotzdem abgewiesen wird. Belegt: `scripts/pakete-pruefen.py`, Abschnitt J. |
| Braucht die Validation Function die Grenze? | nein | Sie prüft nur, **ob** ein Nachweis-Kennzeichen im Warenkorb liegt. Bei einer einzigen Grenze je Laden gibt es nichts zu unterscheiden. |
