@mostajs/auth-dialects 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,19 @@
1
+ # Changelog — @mostajs/auth-dialects
2
+
3
+ Format [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) · [SemVer](https://semver.org/lang/fr/).
4
+ Auteur : Dr Hamid MADANI <drmdh@msn.com>
5
+
6
+ ## [0.1.0] — 2026-06-21
7
+
8
+ ### Added
9
+ - Registre **zéro-dépendance** des « variétés » de connexion (**dialectes**), façon `@mostajs/orm` (DB-agnostique, port `userRepo`).
10
+ - Deux familles (`kind`) : **magic-link** (`mail`, `sms` = *magic-sms-link*, `mail&sms`) et **biometric** WebAuthn (`face`, `finger`, `iris`).
11
+ - `defaultDialects()` — catalogue des 6 variétés ; `mail`/`sms`/`mail&sms` `active`, `face`/`finger`/`iris` `planned` (à câbler via `@mostajs/auth` WebAuthn).
12
+ - `createDialectRegistry({dialects?})` → `list/get/byKind/active/planned/select(identifier)/register(d)` (upsert par key, mise en tête = priorité, **extensible**).
13
+ - Helpers `normalizePhone`, `looksLikePhone`, constantes `KINDS`/`STATUS`.
14
+ - Tests `@mostajs/mjs-unit` (9 verts) + README + llms.txt.
15
+
16
+ ### Notes
17
+ - Extrait de `incubator/app/auth.mjs` (extraction-first §10) — l'app le consomme désormais en composition.
18
+ - WebAuthn ne distingue pas face/finger/iris au niveau protocole (`uv=required`, l'OS choisit) : un seul mécanisme, noms = branding.
19
+ - Placé dans `@mostajs/auth-stack` (avec `sms-lite`/`smtp-lite`/`broadcast` regroupés dans le stack auth).
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @mostajs/auth-dialects
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · Licence : AGPL-3.0-or-later
4
+ Membre du **`@mostajs/auth-stack`** (avec `mosta-auth`, `mosta-auth-lite`, `mosta-auth-flow`, `mosta-sms-lite`, `mosta-smtp-lite`, `mosta-broadcast`).
5
+
6
+ Registre **zéro-dépendance** des **« variétés » de connexion** — les **dialectes** —
7
+ pour l'authentification, dans l'esprit de `@mostajs/orm` (DB-agnostique, **ports**
8
+ injectés). Une variété de login = un **dialecte**.
9
+
10
+ > **Façon dialectes** — même paradigme que les *drivers* de `@mostajs/sms-lite`, les
11
+ > *canaux* de `@mostajs/broadcast` et les *schémas* de `@mostajs/nomenclature` :
12
+ > un registre extensible où l'on **enregistre** des variantes sans toucher au flux.
13
+
14
+ ## Deux familles (`kind`)
15
+
16
+ | Famille | Principe | Variétés |
17
+ |---|---|---|
18
+ | `magic-link` | sans mot de passe — le **lien magique** est **livré** par un/des canal(aux) | `mail`, `sms` (**magic-sms-link**), `mail&sms` |
19
+ | `biometric` | vérification **sur l'appareil** via **WebAuthn/passkeys** (déléguée à `@mostajs/auth`) | `face`, `finger`, `iris` |
20
+
21
+ ## Catalogue par défaut (les 6)
22
+
23
+ | key | kind | status | canaux / mécanisme |
24
+ |---|---|---|---|
25
+ | `mail` | magic-link | active | `email` — `match`=email, `resolve`=findByEmail |
26
+ | `sms` | magic-link | active | `sms` (**magic-sms-link**) — `match`=numéro, `resolve`=findByPhone |
27
+ | `mail&sms` | magic-link | active | `email`+`sms` — comportement par défaut (compte avec email **et** numéro) |
28
+ | `face` | biometric | planned | **face-api.js** (client) + **PWA scan** (caméra) — `provider=face-api`, `capture=pwa-scan` |
29
+ | `finger` | biometric | planned | WebAuthn platform, `uv=required` (`provider=webauthn`) |
30
+ | `iris` | biometric | planned | WebAuthn platform, `uv=required` (`provider=webauthn`) |
31
+
32
+ > **`face` ≠ `finger`/`iris`** : la reconnaissance faciale passe par **face-api.js**
33
+ > (ML côté navigateur) avec **capture caméra en PWA** (`pwa-scan`) — pas de WebAuthn.
34
+ > `finger`/`iris` utilisent l'**authentificateur de plateforme WebAuthn**.
35
+ >
36
+ > **Réalité WebAuthn** (finger/iris) : on **ne peut pas** exiger « iris » plutôt
37
+ > qu'« empreinte » — l'OS choisit la modalité (`userVerification: required`) ;
38
+ > les deux noms sont du **branding/UX** sur un seul mécanisme.
39
+
40
+ ## Usage
41
+
42
+ ```js
43
+ import { createDialectRegistry } from '@mostajs/auth-dialects';
44
+
45
+ const reg = createDialectRegistry();
46
+
47
+ // 1) sélection par identifiant (email ou numéro)
48
+ const dialect = reg.select(identifier); // 'a@b.dz' → mail ; '+213…' → sms
49
+ if (dialect?.resolve) {
50
+ const user = await dialect.resolve(identifier, userRepo); // port { findByEmail, findByPhone }
51
+ if (user) { /* émettre le magic-link pour user.email, livrer selon dialect.channels */ }
52
+ }
53
+
54
+ // 2) introspection (UI / matrice / doc)
55
+ reg.byKind('biometric'); // [face, finger, iris]
56
+ reg.active(); // [mail, sms, mail&sms]
57
+
58
+ // 3) extension — ajouter une variété sans toucher au flux
59
+ reg.register({
60
+ key: 'whatsapp', label: 'WhatsApp', kind: 'magic-link', status: 'active',
61
+ match: (id) => String(id).startsWith('wa:'),
62
+ resolve: (id, repo) => repo.findByPhone(id),
63
+ });
64
+ ```
65
+
66
+ ## API
67
+
68
+ - `createDialectRegistry({ dialects? })` → `{ list, get, byKind, active, planned, select, register }`
69
+ - `defaultDialects()` → les 6 dialectes
70
+ - `normalizePhone(s)` · `looksLikePhone(id)` · `KINDS` · `STATUS`
71
+
72
+ `register(d)` fait un **upsert par `key`** et place le dialecte **en tête**
73
+ (priorité de `select`). DB-agnostique : `resolve` dépend du **port `userRepo`**
74
+ fourni par le consumer.
75
+
76
+ ## Référence d'implémentation
77
+
78
+ `Hadhinat` `incubator/app/auth.mjs` consomme ce registre ; le transport SMS est
79
+ `@mostajs/sms-lite` (méthode **magic-sms-link**) ; la biométrie sera câblée via
80
+ WebAuthn de `@mostajs/auth`. Voir `llms.txt`.
package/llms.txt ADDED
@@ -0,0 +1,54 @@
1
+ # @mostajs/auth-dialects — llms.txt
2
+
3
+ RÔLE
4
+ Registre ZÉRO-DÉPENDANCE des « variétés » de connexion (DIALECTES) pour l'auth — façon @mostajs/orm
5
+ (DB-agnostique, ports injectés). Une variété = un DIALECTE. Deux familles (kind) :
6
+ • magic-link : sans mot de passe, lien magique LIVRÉ par canal — mail, sms (« magic-sms-link »), mail&sms.
7
+ • biometric : vérification sur l'APPAREIL via WebAuthn/passkeys — face, finger, iris — déléguée à @mostajs/auth.
8
+ Le module NE résout PAS les comptes : chaque dialecte magic-link reçoit un PORT userRepo {findByEmail, findByPhone}.
9
+ Version 0.1.0 · AGPL-3.0-or-later · Dr Hamid MADANI <drmdh@msn.com>. Membre du stack @mostajs/auth-stack.
10
+
11
+ EXPORTS (src/index.js)
12
+ KINDS = { MAGIC_LINK:'magic-link', BIOMETRIC:'biometric' }
13
+ STATUS = { ACTIVE:'active', PLANNED:'planned' }
14
+ normalizePhone(s) -> string|null (E.164 : garde « + » + chiffres)
15
+ looksLikePhone(id) -> boolean (email vs numéro)
16
+ defaultDialects() -> Dialect[] (les 6 : mail, sms, mail&sms, face, finger, iris)
17
+ createDialectRegistry({ dialects? }) -> {
18
+ list(), get(key), byKind(kind), active(), planned(),
19
+ select(identifier) -> Dialect|null, (1er dialecte dont match(identifier) est vrai)
20
+ register(dialect) -> registre (upsert par key, mis EN TÊTE → priorité ; chaînable)
21
+ }
22
+
23
+ FORME D'UN DIALECTE
24
+ { key, label, kind, status, channels?:string[], requires?:'sms', notFound?:string,
25
+ match?(id)->bool, resolve?(id, userRepo)->Promise<user|null>, // magic-link sélectionnables par identifiant
26
+ authenticator?:'platform', uv?:'required' } // biométriques (WebAuthn)
27
+ - match/resolve : SEULS les magic-link auto-sélectionnables (mail, sms). L'ordre compte (1re correspondance).
28
+ - sans match() (mail&sms, face, finger, iris) : au CATALOGUE (UI/doc) mais pas auto-sélectionnés par un identifiant.
29
+
30
+ CATALOGUE PAR DÉFAUT (les 6)
31
+ mail magic-link active channels=[email] match=email resolve=findByEmail notFound=unknown_email
32
+ sms magic-link active channels=[sms] requires=sms match=phone resolve=findByPhone notFound=unknown_phone
33
+ mail&sms magic-link active channels=[email,sms] requires=sms (défaut quand un compte a email+numéro)
34
+ face biometric planned provider=face-api capture=pwa-scan (reconnaissance faciale CLIENT via face-api.js + caméra PWA — PAS WebAuthn)
35
+ finger biometric planned provider=webauthn authenticator=platform uv=required (à câbler via @mostajs/auth)
36
+ iris biometric planned provider=webauthn authenticator=platform uv=required
37
+
38
+ USAGE
39
+ import { createDialectRegistry } from '@mostajs/auth-dialects';
40
+ const reg = createDialectRegistry();
41
+ const dialect = reg.select(identifier); // email→mail, numéro→sms
42
+ if (dialect?.resolve) { const user = await dialect.resolve(identifier, userRepo); /* émettre le magic-link pour user.email */ }
43
+ // étendre :
44
+ reg.register({ key:'whatsapp', kind:'magic-link', status:'active', match:id=>id.startsWith('wa:'), resolve:(id,r)=>r.findByPhone(id) });
45
+
46
+ PIÈGES
47
+ - WebAuthn ne permet PAS d'exiger « iris » plutôt que « empreinte » : l'OS choisit la modalité (UV=required).
48
+ face/finger/iris = même mécanisme, noms = branding/UX. 1 seul flux WebAuthn côté serveur.
49
+ - magic-sms-link : le jeton est long → corps SMS en GSM-7 strict (sans accents) sinon UCS-2 multiplie les segments
50
+ (Twilio trial : erreur 30044). Cf. @mostajs/auth llms §LIVRAISON MULTI-CANAL.
51
+ - DB-agnostique : sans userRepo, resolve() ne peut rien faire (port à fournir par le consumer).
52
+
53
+ RÉFÉRENCE D'IMPLÉMENTATION
54
+ Hadhinat incubator/app/auth.mjs (consomme ce registre) ; @mostajs/sms-lite (transport SMS) ; @mostajs/auth (WebAuthn).
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mostajs/auth-dialects",
3
+ "version": "0.1.0",
4
+ "description": "Registre ZÉRO-DÉPENDANCE des « variétés » de connexion (DIALECTES) pour l'auth : magic-link (mail / sms = magic-sms-link / mail&sms) + biométrie WebAuthn (face / finger / iris). DB-agnostique (port userRepo), extensible (register).",
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
+ "login",
22
+ "magic-link",
23
+ "magic-sms-link",
24
+ "sms",
25
+ "passwordless",
26
+ "webauthn",
27
+ "passkey",
28
+ "biometric",
29
+ "dialects",
30
+ "zero-dependency"
31
+ ],
32
+ "devDependencies": {
33
+ "@mostajs/mjs-unit": "^0.3.0"
34
+ },
35
+ "scripts": {
36
+ "test": "bash test-scripts/run-tests.sh"
37
+ }
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,92 @@
1
+ // @mostajs/auth-dialects — registre des « variétés » de connexion (DIALECTES), façon @mostajs/orm pour l'auth.
2
+ // ZÉRO DÉPENDANCE. Une « variété » de connexion = un DIALECTE. Deux familles (kind) :
3
+ // • magic-link : sans mot de passe, le lien magique est LIVRÉ par un/des canal(aux) — mail, sms, mail&sms.
4
+ // • biometric : vérification côté APPAREIL via WebAuthn/passkeys — face, finger, iris — déléguée à @mostajs/auth
5
+ // (startAuthentication/finishAuthentication, authenticator 'platform', userVerification 'required').
6
+ // NB protocole : WebAuthn ne permet PAS d'exiger « iris » plutôt que « empreinte » — l'OS choisit la modalité ;
7
+ // face/finger/iris partagent donc le même mécanisme (UV=required), les noms sont du branding/UX.
8
+ // Le module ne RÉSOUT pas les comptes lui-même : chaque dialecte magic-link reçoit un PORT userRepo
9
+ // ({ findByEmail, findByPhone }) fourni par le consumer (extraction-first, DB-agnostique comme @mostajs/orm).
10
+ // Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
11
+
12
+ export const KINDS = Object.freeze({ MAGIC_LINK: 'magic-link', BIOMETRIC: 'biometric' });
13
+ export const STATUS = Object.freeze({ ACTIVE: 'active', PLANNED: 'planned' });
14
+
15
+ /** Normalise un numéro en gardant « + » et les chiffres (E.164) : « +213 790-37 35 90 » → « +213790373590 ». */
16
+ export function normalizePhone(s) {
17
+ if (!s) return null;
18
+ const p = String(s).replace(/[^\d+]/g, '');
19
+ return p.replace(/\+(?=.*\+)/g, '') || null; // un seul « + », en tête
20
+ }
21
+
22
+ /** Heuristique : un identifiant de connexion est-il un numéro de mobile (vs un email) ? */
23
+ export function looksLikePhone(id) {
24
+ const s = String(id || '').trim();
25
+ if (s.includes('@')) return false;
26
+ return /^\+?[\d][\d\s.\-()]{5,}$/.test(s);
27
+ }
28
+
29
+ /**
30
+ * Catalogue par défaut des 6 variétés. Forme d'un dialecte :
31
+ * { key, label, kind, status, channels?, requires?, notFound?, match?(id)->bool, resolve?(id,userRepo)->Promise<user|null>,
32
+ * authenticator?, uv? }
33
+ * - match/resolve : uniquement les magic-link sélectionnables par identifiant. L'ordre compte (1re correspondance gagne).
34
+ * - sans match() (mail&sms, biométriques) : au CATALOGUE (UI/doc) mais pas auto-sélectionnés par un identifiant.
35
+ */
36
+ export function defaultDialects() {
37
+ return [
38
+ // — magic-link, par canal de livraison —
39
+ {
40
+ key: 'mail', label: 'Email → magic-link', kind: KINDS.MAGIC_LINK, status: STATUS.ACTIVE,
41
+ channels: ['email'], notFound: 'unknown_email',
42
+ match: (id) => String(id).includes('@'),
43
+ resolve: (id, repo) => repo.findByEmail(id),
44
+ },
45
+ {
46
+ key: 'sms', label: 'Mobile → magic-sms-link', kind: KINDS.MAGIC_LINK, status: STATUS.ACTIVE,
47
+ channels: ['sms'], requires: 'sms', notFound: 'unknown_phone',
48
+ match: (id) => looksLikePhone(id),
49
+ resolve: (id, repo) => repo.findByPhone(id),
50
+ },
51
+ {
52
+ key: 'mail&sms', label: 'Email + SMS (double canal)', kind: KINDS.MAGIC_LINK, status: STATUS.ACTIVE,
53
+ channels: ['email', 'sms'], requires: 'sms',
54
+ // Pas de match() : COMPORTEMENT par défaut quand un compte a email + numéro (le port d'envoi livre sur les deux).
55
+ },
56
+ // — biométrie — deux familles de fournisseurs (provider) :
57
+ // • face : reconnaissance faciale côté CLIENT via face-api.js + capture caméra en PWA (« pwa-scan ») — pas de WebAuthn.
58
+ // • finger/iris : authentificateur de plateforme WebAuthn/passkeys (vérification sur l'appareil), à câbler via @mostajs/auth.
59
+ { key: 'face', label: 'Reconnaissance faciale (face-api + PWA scan)', kind: KINDS.BIOMETRIC, status: STATUS.PLANNED, provider: 'face-api', capture: 'pwa-scan' },
60
+ { key: 'finger', label: 'Empreinte digitale (WebAuthn)', kind: KINDS.BIOMETRIC, status: STATUS.PLANNED, provider: 'webauthn', authenticator: 'platform', uv: 'required' },
61
+ { key: 'iris', label: 'Iris (WebAuthn)', kind: KINDS.BIOMETRIC, status: STATUS.PLANNED, provider: 'webauthn', authenticator: 'platform', uv: 'required' },
62
+ ];
63
+ }
64
+
65
+ /**
66
+ * Crée un registre de dialectes (extensible). API :
67
+ * list() → copie du catalogue
68
+ * get(key) → un dialecte | null
69
+ * byKind(kind) → dialectes d'une famille
70
+ * active()/planned() → par statut
71
+ * select(identifier) → 1er dialecte dont match(identifier) est vrai (ou null)
72
+ * register(dialect) → upsert par key (mis EN TÊTE → priorité de match) ; renvoie le registre (chaînable)
73
+ */
74
+ export function createDialectRegistry({ dialects = defaultDialects() } = {}) {
75
+ const list = dialects.map((d) => ({ ...d }));
76
+ const api = {
77
+ list: () => list.map((d) => ({ ...d })),
78
+ get: (key) => list.find((d) => d.key === key) || null,
79
+ byKind: (kind) => list.filter((d) => d.kind === kind).map((d) => ({ ...d })),
80
+ active: () => list.filter((d) => d.status === STATUS.ACTIVE).map((d) => ({ ...d })),
81
+ planned: () => list.filter((d) => d.status === STATUS.PLANNED).map((d) => ({ ...d })),
82
+ select: (identifier) => list.find((d) => typeof d.match === 'function' && d.match(identifier)) || null,
83
+ register(d) {
84
+ if (!d || !d.key) throw new Error('@mostajs/auth-dialects: dialect.key requis');
85
+ const i = list.findIndex((x) => x.key === d.key);
86
+ if (i >= 0) list[i] = { ...list[i], ...d };
87
+ else list.unshift(d); // priorité de correspondance (avant mail/sms)
88
+ return api;
89
+ },
90
+ };
91
+ return api;
92
+ }