npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@parallel-adventure/external-site-sdk

v0.6.0

Published

Client TypeScript pour la façade External Gateway Parallèle (sites externes, sans clé Supabase côté navigateur).

Readme

@parallel-adventure/external-site-sdk

Client TypeScript (ESM) pour intégrer un site externe à la façade HTTP apps/external-gateway. Aucune clé Supabase n’est exposée au navigateur : tout passe par {origin}/v1/....

Quand installer ce SDK ? Ce SDK est l'instrumentation Parallèle d'un site. Il s'installe après que le design, le contenu, et l'UX du site soient validés par le scénariste — pas dès la création du projet. La phase de design est faite par l'éditeur (Lovable, etc.) sans aucune dépendance Parallèle ; l'installation de ce package marque le passage en phase d'instrumentation, où l'on câble l'auth, les stimuli, et la réactivité runtime.

Prérequis

  • Gateway Parallèle déployée (ex. https://api.prll.fr) — voir le README du workspace gateway.
  • Un site_id stable attribué au site (cohérent avec la route /v1/sites/{siteId}/... et, côté runtime, avec external_sites du build quand la section est renseignée).

Installation

Projet hors monorepo :

npm install @parallel-adventure/external-site-sdk

Depuis le monorepo (workspaces) :

npm install

Publication npm (après npm run build et npm run test dans ce package) :

cd packages/external-site-sdk
npm publish --access public

Licence : UNLICENSED (choix produit Parallèle — pas de licence open source sur ce package ; usage selon accord avec l’éditeur).

Démarrage rapide

import {
  ParalleleExternalSiteClient,
  setStoredSiteSessionToken,
  getStoredSiteSessionToken,
} from "@parallel-adventure/external-site-sdk";

const origin = "https://api.prll.fr";
const siteId = "ton-site-id";

const client = new ParalleleExternalSiteClient(origin, siteId);

// 1) Policy (méthodes disponibles pour ce site)
const policy = await client.getPolicy();
console.log(policy.access_methods);

// 2) Exemple : OTP email historique (V1)
const otp = await client.requestOtp("[email protected]");
console.log(otp.status);

const session = await client.verifyOtp("[email protected]", "123456");
if (session.site_session_token) {
  setStoredSiteSessionToken(siteId, session.site_session_token);
}

// 3) État frontend-safe
const token = getStoredSiteSessionToken(siteId);
if (token) {
  const state = await client.getState(token);
  console.log(state.projection);
}

// 4) Stimulus (idempotency_key obligatoire)
if (token) {
  const stim = await client.submitStimulus(token, {
    type: "mon_stimulus_type",
    payload: { origin: "lovable", answer: "..." },
    idempotencyKey: crypto.randomUUID(),
  });
  console.log(stim.status);
}

Reco auth — privilégie le code email. requestOtp + verifyOtp (code 6 chiffres tapé dans l'onglet déjà ouvert) est la voie recommandée. Le mode link (lien magique) est cassé par les passerelles de sécurité email corporate (Mimecast Browser Isolation, Microsoft Safe Links, Proofpoint) : elles consomment l'OTP single-use au pré-fetch et/ou ouvrent le lien en navigateur isolé d'où la session ne revient jamais. Voir requestOtp(…, { mode }) et le catalogue d'accès.

Flux générique start-access / complete-access

Aligné sur le catalogue gateway :

await client.startAccess({
  method: "email_otp",
  payload: { email: "[email protected]", redirect_url: "https://..." },
});

const done = await client.completeAccess({
  method: "email_otp",
  payload: { email: "[email protected]", token: "123456", game_id: undefined },
});

Les méthodes email_claim / phone_claim acceptent un game_id optionnel (hint). Une réponse métier peut arriver avec HTTP 403 (status: access_denied) : le client la retourne comme objet JSON, sans lever d’exception — voir ParalleleGatewayError pour les erreurs transport (error + message).

AUTH-TO-STIMULUS BRIDGE — règle non négociable

Une auth réussie ne suffit PAS à faire progresser le runtime. La gateway crée bien le site_session_token, mais le runtime n8n n'est notifié que si le site poste un stimulus external-site-login. Sans ce stimulus, le step log-in côté Builder reste pending pour toujours — symptôme typique : « le joueur s'est connecté mais aucun message WhatsApp / SMS / email n'arrive ».

UX rapide (depuis SDK 0.4.3)

Ne bloque pas l'écran de chargement sur le stimulus ni sur getState. Le goulet d'étranglement habituel est await verifyOtp puis await submitStimulus (qui attend n8n de bout en bout). Confirme la connexion dès que le token arrive :

import {
  handleLoginSuccess,
  ParalleleExternalSiteClient,
} from "@parallel-adventure/external-site-sdk";

const session = await client.verifyOtp(email, code, gameIdHint);
const { confirmed, runtimeNotification } = handleLoginSuccess(
  client,
  siteId,
  session,
  { username: email },
);

if (confirmed) {
  navigate("/dashboard"); // immédiat — le joueur est connecté
}

// Runtime en arrière-plan (ne pas await avant d'afficher le succès)
void runtimeNotification.then((result) => {
  if (!result) {
    toast.warn("Connexion OK — synchronisation aventure en cours…");
  }
});

handleLoginSuccess persiste le token et lance external-site-login sans bloquer. Pour un contrôle manuel, notifyRuntimeLogin + createLoginIdempotencyKey sont aussi exportés.

Pattern legacy (bloquant — à éviter pour l'UX) :

if (session.status === "site_session_created" && session.site_session_token) {
  setStoredSiteSessionToken(siteId, session.site_session_token);
  await client.submitStimulus(session.site_session_token, {
    type: "external-site-login",
    payload: { logged_in: true, username: <email_or_phone> },
    idempotencyKey: crypto.randomUUID(),
  });
}

Pour la liste des fields que le site doit fournir vs ceux que la gateway remplit toute seule (auto_filled === true depuis v0.3.2), utiliser client.getInputSources(token) et filtrer sur auto_filled === false. Le site ne doit jamais hardcoder un field couvert par auto_filled (cf. Anti-patterns plus bas).

UX d'attente — paliers de patience (depuis SDK 0.6.0)

Règle : quand on peut, on valide tout de suite ; sinon, on fait patienter avec un écran immersif dont le message évolue. Choisis le pattern par action, selon que la réponse gateway conditionne ou non l'écran suivant :

| Cas | Pattern | |---|---| | Le résultat conditionne l'écran suivant (formulaire validé par LLM, quiz, choix branchant → result.validated / result.next_step) | runWithWaitPhases + écran d'attente immersif | | Le résultat ne conditionne rien (page vue, signal décoratif, télémétrie de jeu) | submitStimulusInBackground / submitCustomStimulusInBackground — l'UI avance immédiatement | | Login | handleLoginSuccess (cf. UX rapide ci-dessus) |

Attente bloquante assumée — la réponse prend couramment 3 à 15 s (runtime n8n + validation LLM de bout en bout). runWithWaitPhases notifie l'UI à chaque palier franchi pour faire évoluer le message d'attente :

import { runWithWaitPhases } from "@parallel-adventure/external-site-sdk";

// Messages dans le ton du site — exemple « administration » :
const MESSAGES: Record<string, string> = {
  sending: "Transmission de votre dossier…",
  processing: "Analyse par nos services en cours…",
  slow: "Vérification approfondie — merci de patienter.",
  very_slow: "C'est plus long que prévu. Ne quittez pas cette page.",
};

const result = await runWithWaitPhases(
  () => client.submitCustomStimulus(token, input),
  { onPhase: (key) => setWaitMessage(MESSAGES[key] ?? MESSAGES.processing) },
);

if (result.result?.validated) {
  showScreen(result.result.next_step);
} else {
  showRetry(result.result?.feedback_message);
}

L'échelle par défaut (DEFAULT_WAIT_PHASES) : sending (0 s) → processing (1,5 s) → slow (6 s) → very_slow (15 s). Configurable via phases (clés libres). Les minuteurs sont annulés dès que l'appel aboutit ; une exception dans onPhase n'affecte jamais la soumission.

Envoi en arrière-plan — quand l'écran suivant ne dépend pas de la réponse, ne bloque pas :

import { submitStimulusInBackground } from "@parallel-adventure/external-site-sdk";

showNextScreen(); // immédiat
void submitStimulusInBackground(client, token, {
  type: "external-site-action",
  payload: { action: "poster-consulte" },
}); // clé d'idempotence auto-générée, échec loggé, jamais de rejet

⚠️ Ne bascule jamais en arrière-plan une action dont le step Builder attend la validation pour transitionner l'écran du site : le joueur verrait un état faux. En cas de doute → attente bloquante avec paliers.

Méthodes par cas d'usage

| method | Forme complete-access | Cas typique | |---|---|---| | email_otp | { email, token, game_id? } | OTP email standard (Supabase Auth) | | phone_otp | { phone, token, game_id? } | OTP SMS standard (Supabase Auth) | | magic_link | { email, token, game_id? } | Lien magique email (variant email_otp) | | login_password | { email, password, game_id? } | Email + mot de passe Supabase | | email_claim | { email, game_id? } | Saisie email sans vérification (low) | | phone_claim | { phone, game_id? } | Saisie téléphone sans vérification (low) | | url_context | { game_id, player_id? } | Contexte URL signé / dérivé | | signed_deep_link | { signed_token } ou { token } | JWT HS256 émis par le runtime | | runtime_login_code | { challenge_id, code } ou { phone, code } | Challenge à usage unique (TTL 15 min par défaut), S2S ou lookup-by-phone | | runtime_player_secret | { phone, code, variable_key? } | Stateless : compare au scalaire de runtime_variables.<variable_key> (default ticket_id). Pas de TTL, pas de single-use. Secret dérivé scénariste-side. | | runtime_game_secret | { token, variable_key? } | Stateless, sans identité joueur : résout la PARTIE en matchant token contre runtime_variables.<variable_key> (default webmail_token). Pour hacker le compte d'un PNJ via l'URL nue — pas d'email/tel du joueur, pas de lien. Pas de TTL, rejouable. |

Choix de méthode d'auth selon contexte

Le catalogue ci-dessus est exhaustif, mais le choix se fait sur 4 axes : type d'audience, niveau d'assurance attendu, canal de distribution du contexte, et dépendances infra (Supabase Auth, déploiement Edge Function, etc.).

| Contexte du site | Méthode recommandée | Pourquoi | |---|---|---| | Audience corporate (emails @entreprise filtrés par Mimecast / Safe Links / Proofpoint) | email_otp mode code | Le code 6 chiffres tapé évite les passerelles qui cassent les liens. À éviter : magic_link et email_otp mode link. | | Audience grand public (gmail / outlook personnels) | email_otp mode code ou link | Les deux marchent. code reste plus prévisible et n'oblige pas à un retour dans l'onglet original. | | Inscription minimaliste, MVP, démo | email_claim ou phone_claim | Crée une session sur simple déclaration, sans OTP. Niveau d'assurance bas (low) — ne pas utiliser pour des sites où une fraude d'identité serait problématique. | | Onboarding 1-clic via lien WhatsApp / SMS porté par le runtime | url_context (low-assurance) ou signed_deep_link (high-assurance, JWT HS256) | Le contexte est entièrement porté par l'URL ; le site n'a qu'à appeler completeAccess avec les params lus de l'URL. signed_deep_link valide la signature côté gateway. | | Login secret pré-distribué par le scénariste (ticket dérivé téléphone, code unique par joueur) | runtime_player_secret | Stateless : la gateway compare au scalaire de runtime_variables.<variable_key> (défaut ticket_id). Pas de TTL, pas de single-use, le secret peut être réutilisé. | | Hack du compte d'un PNJ (login d'un tiers ; URL nue ; retour multi-navigateur sans lien) | runtime_game_secret | Un token opaque per-partie (porté par l'email/identifiant saisi) résout la partie côté gateway, sans email/téléphone réel du joueur. Token dérivé scénariste-side (ex. generate-hashed-code sur game_id). | | Challenge à usage unique généré par le runtime (TTL 15 min) | runtime_login_code | Le runtime émet le challenge (via n8n), le site le consomme. single-use strict. | | Email/mot de passe Supabase Auth déjà en place | login_password | Compte préexistant Supabase. Pas de magic link. |

Règle pratique d'arbitrage : démarrer par le plus simple compatible avec le niveau de confiance requis. email_otp mode code est un défaut robuste pour 80 % des cas. Passer à runtime_player_secret ou url_context quand le scénariste a besoin d'un parcours sans saisie d'email.

Exemple runtime_player_secret (Concert Talk / Freeforia) :

const session = await client.completeAccess({
  method: "runtime_player_secret",
  payload: {
    phone: "33612345678",
    code: "1234567890123456", // ticket dérivé du téléphone, sans le `*` décoratif
  },
});

if (session.status === "site_session_created" && session.site_session_token) {
  setStoredSiteSessionToken(siteId, session.site_session_token);
  // Poste ensuite le stimulus external-site-login pour faire avancer le runtime
  await client.submitStimulus(session.site_session_token, {
    type: "external-site-login",
    payload: { logged_in: true, username: "33612345678" },
    idempotencyKey: crypto.randomUUID(),
  });
}

Erreurs

  • ParalleleGatewayError : échec HTTP « enveloppe » (ex. unknown_method, JSON invalide, réseau). Propriétés : httpStatus, code, body.
  • Réponses métier auth : pending_link, ambiguous_player_context, access_denied, etc. sont portées dans le JSON (status, resolution_result, …), pas nécessairement comme exception.

Injection de fetch (tests, SSR) :

const client = new ParalleleExternalSiteClient(origin, siteId, {
  fetch: globalThis.fetch,
});

Anti-patterns

  • Ne pas poster external-site-login après une auth réussie. C'est le bug n°1 — auth OK côté gateway mais runtime jamais notifié. Cf. AUTH-TO-STIMULUS BRIDGE plus haut.
  • Hardcoder un field couvert par auto_filled === true. Depuis v0.3.2, la gateway enrichit automatiquement le payload des stimulus globaux (external-site-login, external-form-submit, external-gps-check-in, external-option-selected) avec les fields canoniques (auth_method, resolution_result, user_agent, ip_hashed, session_proof, logged_in_at, …). Les poser côté site crée un risque de désaccord auth/runtime. Toujours appeler getInputSources(token) et filtrer sur auto_filled === false avant de construire le payload.
  • Confondre submitStimulus (catalogue plateforme global) et submitCustomStimulus (custom_stimulus_types du build). Les premiers ont un schéma fixe partagé entre toutes les aventures ; les seconds sont définis par le scénariste dans le Builder pour CE site. Vérifier le kind de chaque entrée renvoyée par getInputSources ("global" ou "custom").
  • Hardcoder le site_id dans le code du site au lieu de le passer en variable d'env (VITE_PARALLELE_SITE_ID, NEXT_PUBLIC_PARALLELE_SITE_ID, …). Bloque le déploiement multi-environnement.
  • Oublier le gameId hint dans verifyOtp quand le site connaît l'aventure ciblée (lue depuis ?game_id= de l'URL d'invitation). Crée des ambiguous_player_context évitables.
  • Polling de getState pour la réactivité UI sans lire state.projection.vars. Depuis v0.3.4, les runtime_variables exposées par le scénariste arrivent dans state.projection.vars — c'est le canal de réactivité (déverrouillage de bouton, bandeau d'alerte, etc.). Comparaison stricte (=== "true") pour les flags STRING.
  • Utiliser mode: "link" pour une audience corporate. Mimecast / Microsoft Safe Links / Proofpoint pré-fetchent le lien et consomment l'OTP single-use. Privilégier mode: "code" (cf. Reco auth en démarrage rapide).

Documentation canonique (monorepo)

| Sujet | Document | |-------|----------| | Contrat HTTP V1 | docs/architecture/external-sites-api-contracts.md | | Catalogue méthodes / assurance | docs/architecture/external-sites-access-catalog.md | | Guidelines intégration | docs/architecture/external-sites-integration-guidelines.md | | Playbook intégrateur | docs/architecture/external-sites-integrator-playbook.md | | Prompt Lovable | docs/implementation/external-sites-lovable-prompt.md | | Maintenance interne (évolution, semver, publication) | docs/implementation/external-site-sdk-maintenance.md |

Compatibilité

  • Module : ESM uniquement ("type": "module").
  • TypeScript : module / moduleResolution NodeNext — imports sources avec suffixe .js (résolution vers .ts au build).

Scripts

npm run build
npm run test