@mostajs/auth-magic-qr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog — @mostajs/auth-magic-qr
2
+ ## [0.1.0] — 2026-06-24
3
+ ### Ajouté
4
+ - Badge persistant (jeton signé HMAC + verifyBadge timing-safe + expiration) et lien magique éphémère, en composant @mostajs/auth (magic-link) + @mostajs/qrpanel (rendu) par injection. 4 tests. Membre de mosta-auth-stack.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @mostajs/auth-magic-qr
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
+
5
+ Connexion **par QR — option A** : le QR **encode un lien de connexion**. Deux usages, pilotables par l'hôte (`.env`) :
6
+ - **Badge persistant** — QR d'un **jeton signé HMAC** (`userId.exp.sig`) → URL `/login/qr?token=…` ; l'hôte le vérifie et ouvre une session. Idéal pour un **badge physique** (pépinière, événement). `badgeTtl=0` = sans expiration.
7
+ - **Lien éphémère** — QR d'un **magic-link frais** (`@mostajs/auth`) pour **affichage à l'écran** (scan = connexion sur mobile).
8
+
9
+ **Composition (DEVRULES §10)** : ne réimplémente RIEN — le **magic-link** vient de `@mostajs/auth` (injection `requestMagicLink`) et le **rendu QR** de `@mostajs/qrpanel` (injection `renderQr`). Membre de `mosta-auth-stack` (à côté de `@mostajs/auth`, `auth-lite`, `auth-flow`).
10
+
11
+ ```js
12
+ import { createAuthMagicQr } from '@mostajs/auth-magic-qr';
13
+ import { qrLive } from '@mostajs/qrpanel';
14
+
15
+ const aq = createAuthMagicQr({
16
+ secret: process.env.AUTH_QR_SECRET, baseUrl: 'https://market.amia.fr',
17
+ badgeTtl: 0, renderQr: (url) => qrLive(url, 'Scanner pour se connecter'),
18
+ requestMagicLink: (id) => auth.requestLink(id), // @mostajs/auth
19
+ });
20
+ const badge = aq.badgeQr('user-42'); // QR de badge (à imprimer)
21
+ // endpoint : GET /login/qr?token=… → const { userId } = aq.verifyBadge(token) ; ouvrir session.
22
+ ```
23
+
24
+ API : voir `llms.txt`. Tests : `npm test` (4). AGPL-3.0-or-later.
package/llms.txt ADDED
@@ -0,0 +1,11 @@
1
+ # @mostajs/auth-magic-qr
2
+ Connexion par QR (option A : le QR ENCODE un lien). Compose @mostajs/auth (magic-link) + @mostajs/qrpanel (rendu) par INJECTION. Membre de mosta-auth-stack. Pilotable .env.
3
+ ## API
4
+ createAuthMagicQr({ secret, baseUrl, badgeTtl=0, renderQr, requestMagicLink, path='/login/qr', now }) →
5
+ - badgeToken(userId) → "userId.exp.sigHMAC" (badge PERSISTANT ; badgeTtl=0 = sans expiration)
6
+ - verifyBadge(token) → { userId } | null (signature timing-safe + expiration)
7
+ - badgeUrl(userId) → `${baseUrl}${path}?token=…`
8
+ - badgeQr(userId, opts) → renderQr(url) si injecté sinon url
9
+ - loginLinkQr(identifier, opts) → { url, qr } : magic-link FRAIS (éphémère, écran) via requestMagicLink injecté
10
+ ## Usage hôte
11
+ Badge : imprimer badgeQr(userId) ; endpoint GET /login/qr?token=… → verifyBadge → ouvrir session. Écran : afficher loginLinkQr(email).qr.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@mostajs/auth-magic-qr",
3
+ "version": "0.1.0",
4
+ "description": "Connexion par QR (option A) : le QR encode un lien de connexion — badge PERSISTANT (jeton signé HMAC → endpoint de session) ou lien magique ÉPHÉMÈRE (écran). Compose @mostajs/auth (magic-link) + @mostajs/qrpanel (rendu) par injection. Membre de mosta-auth-stack. Pilotable par .env.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "files": [
10
+ "src",
11
+ "llms.txt",
12
+ "README.md",
13
+ "CHANGELOG.md"
14
+ ],
15
+ "exports": {
16
+ ".": "./src/index.js"
17
+ },
18
+ "keywords": [
19
+ "mostajs",
20
+ "auth",
21
+ "qr",
22
+ "login",
23
+ "badge",
24
+ "magic-link",
25
+ "passwordless"
26
+ ],
27
+ "devDependencies": {
28
+ "@mostajs/mjs-unit": "^0.3.0"
29
+ },
30
+ "scripts": {
31
+ "test": "bash test-scripts/run-tests.sh"
32
+ }
33
+ }
package/src/index.js ADDED
@@ -0,0 +1,57 @@
1
+ // @mostajs/auth-magic-qr — connexion par QR (option A : le QR ENCODE un lien de connexion). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Deux usages, pilotables par l'hôte (via .env) :
3
+ // • badge PERSISTANT : QR d'un jeton signé (HMAC) → URL /login/qr?token=… → l'hôte vérifie et ouvre une session.
4
+ // • lien ÉPHÉMÈRE : QR d'un magic-link frais (@mostajs/auth) pour affichage à l'écran.
5
+ // COMPOSE (injection) : un générateur de magic-link (@mostajs/auth) et un rendu QR (@mostajs/qrpanel) — ne les réimplémente pas (DEVRULES §10).
6
+ import crypto from 'node:crypto';
7
+
8
+ const b64url = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
9
+ const timingEqual = (a, b) => { const x = Buffer.from(String(a)), y = Buffer.from(String(b)); return x.length === y.length && crypto.timingSafeEqual(x, y); };
10
+
11
+ /**
12
+ * @param secret secret HMAC des jetons de badge (REQUIS pour le badge).
13
+ * @param baseUrl base des URLs (ex. https://market.amia.fr).
14
+ * @param badgeTtl validité du badge en secondes (0 = sans expiration — un badge physique).
15
+ * @param renderQr (data, opts) => string|dataURL — injecté (qrpanel qrSvg/qrLive/qrPng) ; sinon renvoie l'URL brute.
16
+ * @param requestMagicLink (identifier) => Promise<{url}|string> — injecté (@mostajs/auth) pour le QR éphémère.
17
+ * @param path chemin de l'endpoint de vérification du badge (def. '/login/qr').
18
+ * @param now injection horloge (tests).
19
+ */
20
+ export function createAuthMagicQr({ secret, baseUrl = '', badgeTtl = 0, renderQr = null, requestMagicLink = null, path = '/login/qr', now = () => Date.now() } = {}) {
21
+ // ── Badge persistant : jeton "userId.exp.signature" (HMAC-SHA256). ──
22
+ function badgeToken(userId) {
23
+ if (!secret) throw new Error('auth-magic-qr: secret requis pour les badges');
24
+ const exp = badgeTtl ? Math.floor(now() / 1000) + badgeTtl : 0;
25
+ const uid = b64url(Buffer.from(String(userId), 'utf8')); // encode (un email contient des '.', qui sont le séparateur)
26
+ const payload = `${uid}.${exp}`;
27
+ const sig = b64url(crypto.createHmac('sha256', secret).update(payload).digest());
28
+ return `${payload}.${sig}`;
29
+ }
30
+ /** Vérifie un jeton de badge → { userId } ou null (signature/expiration). */
31
+ function verifyBadge(token) {
32
+ if (!token || !secret) return null;
33
+ const parts = String(token).split('.');
34
+ if (parts.length !== 3) return null;
35
+ const [uid, exp, sig] = parts;
36
+ const expected = b64url(crypto.createHmac('sha256', secret).update(`${uid}.${exp}`).digest());
37
+ if (!timingEqual(sig, expected)) return null;
38
+ if (Number(exp) && Number(exp) < Math.floor(now() / 1000)) return null;
39
+ let userId; try { userId = Buffer.from(uid.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'); } catch { return null; }
40
+ return { userId };
41
+ }
42
+ const badgeUrl = (userId) => `${baseUrl}${path}?token=${encodeURIComponent(badgeToken(userId))}`;
43
+ /** QR du badge (persistant) — string SVG/PNG si renderQr injecté, sinon l'URL. */
44
+ const badgeQr = (userId, opts) => { const url = badgeUrl(userId); return renderQr ? renderQr(url, opts) : url; };
45
+
46
+ /** QR d'un lien magique FRAIS (éphémère, pour l'écran) → { url, qr }. */
47
+ async function loginLinkQr(identifier, opts) {
48
+ if (!requestMagicLink) throw new Error('auth-magic-qr: requestMagicLink requis pour le QR éphémère');
49
+ const r = await requestMagicLink(identifier);
50
+ const url = (r && r.url) ? r.url : String(r);
51
+ return { url, qr: renderQr ? renderQr(url, opts) : url };
52
+ }
53
+
54
+ return { badgeToken, verifyBadge, badgeUrl, badgeQr, loginLinkQr };
55
+ }
56
+
57
+ export default createAuthMagicQr;