quest'n'click 4.5.0 · r1

Construite pour quest'n'click 4.5.0, révision r1. Si elle est publiée, et laquelle est la courante, est dit par publication.json.

Installer et lancer le Bridge

English · Type : procédure · Public : administrateur · Contexte : Reality Bridge · Statut : prête · S'applique depuis : 4.3.0

Le Reality Bridge, profil local (SQLite) · contrat : stable · vérifié par la CI (matrice de support de la 4.4.0, 2026-10-23) : Node 22.13+ avec node:sqlite, sous Ubuntu ; Chromium et WebKit de bout en bout · vérifications automatiques : jobs reality, reality-xcheck ; plan de CI reality ; 2 fichiers de tests · passes humaines : aucune · limites connues : bridge-init-force, bridge-sql-no-retention, bridge-worker-not-a-sandbox

Le Reality Bridge, profil distributed (Postgres) · contrat : expérimental · vérifié par la CI (matrice de support de la 4.4.0, 2026-10-23) : Postgres 16 en CI ; Le profil de qualification : HTTPS, trois instances (ops/qualify/) · vérifications automatiques : jobs bridge-postgres, deployment-smoke ; plan de CI reality ; 1 fichier de tests · passes humaines : bridge-postgres-https pas faite (#77) · limites connues : bridge-init-force, bridge-sql-no-retention, bridge-worker-not-a-sandbox

Cette page vous mène de rien à un Bridge que vos joueurs atteignent en HTTPS, puis décrit les routes du joueur, les stockages, la conservation, le déploiement de référence et les classements de speedrun qu’il peut aussi servir. Chaque commande et option : La ligne de commande du Bridge.

Vue d’ensemble

Le Reality Bridge de référence (bridge/src/, le paquet quest-n-click-bridge) sert un jeu. C’est un petit service Node : appairage, connecteurs sous capacités Biscuit, un webhook de démonstration, des événements signés, des Server-Sent Events. Depuis la 4.1.10 il garde son état dans un stockage (le journal JSON-lines de la 4.1.9, SQLite ou Postgres), et un déploiement peut faire tourner plusieurs instances et servir plusieurs tenants. Ce qu’est un Bridge, pour les auteurs : Le Reality Bridge : ce que c’est et Signaux et manifeste. Pourquoi il est construit ainsi : docs/dev/THREAT-MODEL.md et docs/dev/threat-models/constellation.md.

Ce que vous obtenez

Un processus serve qui écoute sur 127.0.0.1:8787 derrière votre proxy inverse HTTPS, avec les clés et la configuration d’un jeu dans un dossier privé, son état dans un stockage (SQLite recommandé), qui répond à /readyz, prêt pour chaque connecteur à qui vous donnez un jeton. Le reality.bridge du jeu pointe vers son URL publique.

Contexte et versions

Prérequis

Où lancer les commandes

Sur la machine qui fait tourner le Bridge, à l’un de ces trois endroits :

OùLa commandeLe manifeste du jeu vient de
Le dépôt du moteurnpm run bridge -- <commande> (avec GAME=<id> pour un autre jeu que le jeu courant)le contenu du jeu
Un projet créénpx quest-n-click bridge <commande>, une fois quest-n-click-bridge installé à côté du moteurle contenu du jeu
Le paquet autonome, sur un serveurnpx quest-n-click-bridge <commande>init --manifest=<jeu>/dist/reality-manifest.json

Le paquet quest-n-click-bridge est l’archive quest-n-click-bridge-<version>.tgz jointe à une release : un module JavaScript, ses politiques Datalog et son schéma SQL. Il lui faut le WebAssembly de Biscuit et zod ; pg seulement pour Postgres, @opentelemetry/api seulement pour les mesures. Les exemples ci-dessous utilisent la forme du dépôt du moteur.

Étapes

  1. Sur un serveur, installez le paquet depuis l’archive de la release (dans le dépôt du moteur ou un projet créé, sautez cette étape) :

    # sur le serveur, dans le dossier du Bridge (par exemple /srv/bridge)
    npm install ./quest-n-click-bridge-<version>.tgz
    

En lancer un

  1. Créez les clés et la configuration. Sur Internet, laissez de côté les webhooks de démonstration et gardez l’état dans SQLite :

    # dans le dépôt du moteur
    npm run bridge -- init --store=sqlite --no-demo-webhooks --audience=bridge.example.org --origin=https://game.example.org
    

    init lit le manifeste du jeu et écrit, sous --dir (.cache/bridge par défaut), en mode 600 :

    • config.json : le manifeste et son empreinte, la clé publique de la racine Biscuit, la clé qui signe les événements (Ed25519, avec un kid), l’empreinte du jeton de l’opérateur, le stockage, et un webhook de démonstration par source sauf avec --no-demo-webhooks (son secret et son propre jeton de connecteur : tout joueur des signaux de cette source, peut confirmer des appairages, pendant 30 jours ou --demo-days) ;
    • root.key : la moitié privée de la racine Biscuit, lue par grant seulement. serve ne la charge jamais : gardez-la là où le Bridge n’est pas, si vous le pouvez ;
    • admin-token : le jeton de l’opérateur, seule copie en clair.

    Rien de secret n’est affiché. .cache/ est ignoré par git. Sans --store, init écrit toujours la configuration du journal JSON-lines, par compatibilité.

Limite · bridge-init-force init refuse d’écraser un config.json existant. --force le remplace, et chaque clé et chaque jeton avec lui : chaque connecteur a besoin d’un nouveau jeton et chaque joueur d’un nouvel appairage.

  1. Lancez-le sur la boucle locale :

    # dans le dépôt du moteur
    npm run bridge -- serve --port=8787 --host=127.0.0.1 --trust-proxy
    

    Il journalise {"event":"bridge.listening",…,"store":"sqlite","pid":…} et continue de tourner. SIGTERM ou Ctrl-C l’arrête : les flux se terminent, le serveur se ferme, le stockage et ses verrous sont libérés, code de sortie 0. Faites-le tourner sous un superviseur (systemd, la politique de redémarrage d’un conteneur).

  1. Mettez-le derrière HTTPS. Le proxy inverse termine TLS, transmet à 127.0.0.1:8787 et ne met pas en tampon text/event-stream. --trust-proxy fait voir aux limites par adresse l’adresse du client (X-Forwarded-For) :

    • --trust-proxy seul ne fait confiance qu’à la boucle locale ;
    • --trust-proxy=10.0.0.0/8,192.0.2.7 nomme les proxies (adresses ou réseaux IPv4) : l’en-tête n’est lu que venant d’eux, et le client est son adresse la plus à droite qui n’en est pas un.

    Un proxy ailleurs que sur la boucle locale (le routeur d’un PaaS) doit être nommé : sinon tous les clients partagent l’adresse du proxy et son unique compartiment de limite. --origin (à init) liste le site du jeu pour CORS ; les routes du joueur ne répondent qu’à lui.

  1. Mettez l’URL publique du Bridge (https://bridge.example.org/) dans le reality.bridge du jeu, puis construisez et publiez le jeu (Signaux et manifeste).
  1. Donnez à chaque connecteur son propre jeton, aussi étroit que sa tâche, et remettez-le sous forme de fichier :

    # dans le dépôt du moteur
    npm run bridge -- grant --connector=mail-1 --source=mail --signals=mail.answer.correct,mail.answer.wrong --pair > mail-1.token
    

    Puis lancez le connecteur (Vue d’ensemble des connecteurs).

Résultat attendu, et comment le vérifier

Pour vérifier un appairage, lisez le journal. N’appelez jamais GET /v1/pairings/<code> : c’est la récupération unique du joueur, et l’appeler remet, et consomme, la capacité du joueur.

Erreurs fréquentes

MessageCauseCorrection
✖ the game declares no reality.signals: nothing to bridgeLe jeu courant n’a pas de reality, ou l’init autonome n’a pas de --manifestChoisir le jeu (GAME=<id>), ou passer --manifest=<jeu>/dist/reality-manifest.json
✖ …/config.json exists (--force replaces it, and every key and token with it)init lancé deux foisGarder l’existant ; --force seulement pour tout recommencer
the game's manifest (…) is not the one configured (…) au serveLes signaux du jeu ont changé depuis initUn nouvel init dans un nouveau dossier pour le nouveau manifeste, puis de nouveaux jetons et appairages
limits.<nom> must be an integer of at least <n>, not <valeur>, ou limits.<nom> is no limit of the Bridge (…)Une mauvaise valeur dans les limits de config.json (vérifiées depuis la 4.3.0)Corriger la valeur, ou la retirer pour garder le défaut (CLI)
journal in use by process <pid> (…lock): one Bridge per journalUn autre Bridge (ou compact) tient le journalL’arrêter ; si ce numéro de processus appartient désormais à un processus sans rapport après un plantage, regarder, puis supprimer le verrou
✖ … : not servingUne restauration ou une migration descendante tient le stockage (un bail de maintenance)Attendre qu’elle se termine
✖ the leaderboards and the daily challenge keep their records in SQL: …Une section runs ou daily sur le journalPasser à SQLite (Stockages)
✖ the Reality Bridge is a separate package: npm install quest-n-click-bridgeDans un projet créé, le paquet manqueL’installer à côté du moteur

Fichiers ou données modifiés

Reprise ou retour en arrière

Côté joueur

Les routes qu’appelle le jeu (CORS pour les sites de --origin seulement) :

Les routes des connecteurs (POST /v1/signals, POST /v1/pairings/<code>/confirm, POST /v1/hooks/<source>) : Vue d’ensemble des connecteurs.

Stockages et profils

ProfilStockagePour
local (stable)SQLite (node:sqlite, Node 22.13+, un fichier, WAL)une machine, un ou quelques processus
(4.1.9)le journal JSON-linesune configuration écrite avant la 4.1.10, ou init sans --store ; un processus
distributed (expérimental)Postgres (pg)plusieurs instances derrière un répartiteur de charge

Conservation, export et suppression

Limite · bridge-sql-no-retention Un stockage SQL garde tous les signaux : compact est la commande du journal et refuse un stockage SQL. Supprimer un joueur retire ses lignes du stockage, pas des fichiers de sauvegarde déjà écrits : c’est à vous de les faire expirer.

Supprimer les données d’un joueur partout (le Bridge, une boîte mail, une configuration SSH, la mémoire des connecteurs, l’appareil) : Vie privée et La ligne de commande du Bridge.

Le déploiement de référence (4.1.18)

ops/qualify/ est le profil distributed sous forme de déploiement reproductible : un proxy HTTPS (Caddy), trois Bridges construits depuis l’archive quest-n-click-bridge, un Postgres, deux tenants, chaque image épinglée par empreinte, les Bridges en lecture seule et sans privilège, le mot de passe et l’URL de la base en fichiers secrets (BRIDGE_STORE_FILE), Postgres jamais publié. npm run ops:qualify (dans le dépôt du moteur, avec Docker) le monte, tue une instance pendant des propositions, tient un flux sur une autre, vérifie que les tenants restent séparés, puis sauvegarde, détruit la topologie avec sa base et la restaure (ops/qualify/README.md). Le profil distributed reste expérimental tant qu’une passe humaine derrière un vrai domaine (24 à 48 heures) n’a pas réussi.

Classements et défi du jour (4.1.16)

La section runs d’une configuration monte /v1/runs ; sa section daily monte /v1/daily, /v1/commit et /v1/reveal/<id>. Les deux gardent leurs enregistrements dans le stockage SQL (--store=sqlite:<fichier> ou BRIDGE_STORE=postgres://… ; serve les refuse sur le journal), migrations 0002 à 0004. Ce qu’elles font pour les joueurs et les auteurs (soumettre un run, le classement, la graine du jour, les mondes Mystère) : Speedrun pour les auteurs.

"runs": {
  "games": { "reference": { "dir": "/srv/games/reference", "fingerprint": { "logic": "…", "trustedExtensions": "…", "presentation": "…", "engine": "…" } } },
  "worker": ["/usr/bin/node", "/srv/quest-n-click/node_modules/tsx/dist/cli.mjs", "/srv/quest-n-click/tools/speedrun/worker.ts"],
  "workers": 2
},
"daily": { "games": { "reference": { "daily": "daily", "mystery": "mystery" } }, "kid": "daily-2026", "keyFile": "daily.jwk", "secretFile": "daily.secret" }

Le worker des speedruns

Chaque run est vérifié par un processus worker que lance le Bridge (la commande worker). Le refus du réseau à l’intérieur du processus worker est une défense en profondeur, pas un bac à sable : un rejeu exécute le code du jeu, et un paquet ou un run hostile pourrait encore atteindre le système de fichiers, lancer un processus ou ouvrir une socket autrement. Faites tourner la commande du worker dans l’isolation du déploiement, par exemple :

docker run --rm -i --network none --read-only --tmpfs /tmp:rw,size=64m --cap-drop ALL --security-opt no-new-privileges \
  --pids-limit 64 --memory 512m --memory-swap 512m --cpus 1 --user 65534:65534 -v /srv/games:/srv/games:ro web-scumm-worker \
  timeout -s KILL 70 node tools/speedrun/worker.ts

comme commande worker : pas d’espace de noms réseau, une racine en lecture seule, un dossier temporaire borné, aucune capability, aucun secret du Bridge dans son environnement (le Bridge ne transmet que PATH, GAME_DIR et NODE_OPTIONS), des limites sur le CPU, la mémoire (pas de swap au-delà), les processus et le temps (le Bridge tue le groupe de processus à timeoutMs + 5 s). Sa sortie standard est plafonnée à 1 Mo et sa sortie d’erreur n’est pas gardée. Tout dossier monté est lisible à l’intérieur : gardez le dossier du Bridge (config.json, ses clés et ses fichiers de jetons) hors du dossier du moteur monté sur /app. Tuer le client docker n’arrête pas son conteneur : c’est timeout -s KILL à l’intérieur (un peu au-delà du timeoutMs du Bridge) qui termine un rejeu trop long, pour que les conteneurs dépassés ne s’accumulent jamais.

Depuis la 4.1.17, ce profil est une fonction (tools/speedrun/container.mjs, workerContainerArgs) :

# dans le dépôt du moteur
node tools/speedrun/container.mjs --image=<image> --app=<engine folder> [--games=<folder>]

affiche la commande worker, sans réseau sauf avec --allow-network (qui le dit sur la sortie d’erreur). Le job worker-container de la CI (npm run e2e:worker-container, Linux et Docker) y fait tourner le run de référence, puis un script hostile : HTTP, DNS et TCP refusés, rien d’écrit hors de /tmp, les secrets de l’hôte invisibles, un processus enfant permis mais sans réseau, le conteneur tué par son propre délai sans rien laisser tourner ; puis un run de nouveau.

Limite · bridge-worker-not-a-sandbox Le worker refuse fetch, WebSocket, TCP, UDP et DNS dans son propre processus, comme deuxième ligne de défense. Ce n’est pas un bac à sable : l’isolation, c’est le conteneur (ou équivalent) dans lequel vous le lancez.

Modération

runs.adminTokenFile nomme le fichier du jeton qui modère les classements d’un tenant (POST /v1/runs/<id>/moderate, Authorization: Bearer …) : à côté de config.json, en mode 0600, au moins 32 caractères. serve refuse de démarrer quand il manque, est plus court, ou est ouvert aux autres (sur les systèmes POSIX ; sous Windows, c’est l’ACL du dossier qui décide). Sans lui, pas de route de modération (404, rien de compté). Le jeton est comparé sous forme d’empreintes de même longueur (sa longueur ne dit rien non plus) ; chaque refus est une ligne d’audit (run.moderation-refused, jamais le porteur), et /healthz compte les modérations acceptées et refusées.

Références


Modifier la source de cette page · Signaler une erreur