@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 +19 -0
- package/README.md +80 -0
- package/llms.txt +54 -0
- package/package.json +38 -0
- package/src/index.js +92 -0
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
|
+
}
|