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

@mostajs/auth-flow

v0.1.0-alpha.3

Published

OAuth 2.0 Device Authorization Grant (RFC 8628) + Authorization Code with PKCE (RFC 8252) — provider-agnostic, reusable across CLIs / SDKs / mobile apps. Token issuance delegated via DI to host (e.g. @mostajs/api-keys).

Readme

@mostajs/auth-flow

v0.1.0-alpha — OAuth 2.0 Device Authorization Grant (RFC 8628) + Authorization Code Flow with PKCE (RFC 8252), provider-agnostic, réutilisable dans tout CLI / SDK / app mobile / agent embarqué.

License

Auteur : Dr Hamid MADANI [email protected] Statut : alpha — interfaces gelées, implémentation à venir (cf. docs/PLAN.md).


Pourquoi ce module

Quand un client (CLI, SDK serveur, app mobile, agent IA) doit s'authentifier auprès d'un service distant sans mot de passe stocké en local et sans formulaire dans le terminal, le standard 2025 est l'OAuth 2.0 Device Flow + PKCE — exactement le pattern qu'utilisent Claude Code, GitHub CLI, Stripe CLI, gcloud, AWS SSO, Vercel.

@mostajs/auth-flow implémente ces deux RFC côté client ET côté serveur, sans connaître la nature du token émis (apikey opaque, JWT, autre) ni la nature de l'utilisateur (free, paid, anonyme). Tout est DI : le host branche les pièces qui lui sont propres.


Architecture

┌──────────────────────────────────────────────────────────────────────────┐
│                                                                           │
│  CLIENT (CLI / SDK)                                                       │
│  ─────────────────                                                        │
│   import { createDeviceFlow } from '@mostajs/auth-flow/client'           │
│                                                                           │
│   ┌────────────────────────────────────────────────────────┐             │
│   │  createDeviceFlow({                                    │             │
│   │    host: 'octonet',                                    │             │
│   │    authorizeEndpoint, tokenEndpoint, clientId, scope,  │             │
│   │    onCodeIssued(code, uri),                            │             │
│   │    store: createFsAuthStore(),                         │             │
│   │  })                                                    │             │
│   └────────────────────────────────────────────────────────┘             │
│                            │                                              │
│                            ▼ HTTP                                         │
│  ─────────────────────────────────────────────────────────                │
│  SERVER (Octonet / autre service)                                         │
│  ─────────────────────────────────                                        │
│   import { createDeviceFlowHandlers } from '@mostajs/auth-flow/server'   │
│                                                                           │
│   ┌────────────────────────────────────────────────────────┐             │
│   │  createDeviceFlowHandlers({                            │             │
│   │    issueAccessToken: ({accountId, scopes, clientId})   │             │
│   │       → { token, expiresIn },           ← @mostajs/api-keys          │
│   │    resolveUserSession(req),             ← @mostajs/auth              │
│   │    onAnonymousVisitor(deviceCode, req), ← @mostajs/payment           │
│   │    pendingStore: createOrmPendingStore(),                            │
│   │  })                                                    │             │
│   └────────────────────────────────────────────────────────┘             │
│                                                                           │
│   POST /device/authorize  → { device_code, user_code, verification_uri } │
│   POST /device/token      → polling — { access_token } once approved    │
│   GET  /device            → page HTML (vanilla, override-able)          │
│   POST /device/approve    → endpoint appelé par /device après consent   │
│                                                                           │
└──────────────────────────────────────────────────────────────────────────┘

Diagramme de séquence — Device Flow (RFC 8628)

┌────────────┐         ┌──────────────┐         ┌────────┐         ┌────────────┐
│ CLIENT CLI │         │ AUTH-FLOW    │         │ BROWSER│         │ USER       │
│ (data-plug)│         │ SERVER       │         │ (mobile│         │            │
│            │         │ (octonet-cl) │         │  / desk)│        │            │
└─────┬──────┘         └──────┬───────┘         └───┬────┘         └─────┬──────┘
      │                       │                     │                    │
      │ ① POST /device/        │                     │                    │
      │    authorize           │                     │                    │
      │ {client_id, scope}     │                     │                    │
      ├──────────────────────▶ │                     │                    │
      │                        │ ⓐ generate          │                    │
      │                        │   device_code       │                    │
      │                        │   user_code         │                    │
      │                        │   pendingStore.put  │                    │
      │ ② DeviceCodeResponse   │                     │                    │
      │ {device_code,          │                     │                    │
      │  user_code: "WDJB-MJHT"│                     │                    │
      │  verification_uri,     │                     │                    │
      │  expires_in, interval} │                     │                    │
      │ ◀──────────────────────│                     │                    │
      │                        │                     │                    │
      │ ③ display URL + code   │                     │                    │
      │  to user (terminal)    │                     │                    │
      ├──────────────────────────────────────────────────────────────────▶│
      │                        │                     │                    │
      │ ④ POLL POST /device/   │                     │                    │
      │    token (interval=5s) │                     │                    │
      ├──────────────────────▶ │                     │                    │
      │ {error: "auth_pending"}│                     │                    │
      │ ◀──────────────────────│                     │                    │
      │                        │                     │ ⑤ open verif.URI   │
      │                        │                     │ ◀──────────────────│
      │                        │                     │                    │
      │                        │ ⑥ GET /device       │                    │
      │                        │ ◀───────────────────│                    │
      │                        │ HTML page (vanilla) │                    │
      │                        │ ───────────────────▶│                    │
      │                        │                     │                    │
      │                        │   ⑦ user enters     │                    │
      │                        │      user_code      │                    │
      │                        │   ⑧ resolveUser     │                    │
      │                        │      Session(req)   │                    │
      │                        │   ⓑ if anonymous:   │                    │
      │                        │      onAnonymous    │                    │
      │                        │      Visitor() →    │                    │
      │                        │      signup/Stripe  │                    │
      │                        │      via @mostajs/  │                    │
      │                        │      payment        │                    │
      │                        │                     │                    │
      │                        │ ⑨ POST /approve     │                    │
      │                        │ ◀───────────────────│                    │
      │                        │ ⓒ pendingStore     │                    │
      │                        │   .approve(code,    │                    │
      │                        │   accountId)        │                    │
      │                        │ "Return to your     │                    │
      │                        │  client" ───────────▶│                   │
      │                        │                     │                    │
      │ ⑩ POLL /device/token  │                     │                    │
      ├──────────────────────▶ │                     │                    │
      │                        │ ⓓ pendingStore     │                    │
      │                        │   .consume()        │                    │
      │                        │ ⓔ issueAccessToken  │                    │
      │                        │   ({accountId,      │                    │
      │                        │    scopes,          │                    │
      │                        │    clientId})       │                    │
      │                        │   → @mostajs/       │                    │
      │                        │     api-keys.       │                    │
      │                        │     create()        │                    │
      │ ⑪ TokenResponse        │                     │                    │
      │ {access_token,         │                     │                    │
      │  expires_in,           │                     │                    │
      │  token_type:"Bearer"}  │                     │                    │
      │ ◀──────────────────────│                     │                    │
      │                        │                     │                    │
      │ ⑫ store.save(...)     │                     │                    │
      │  ~/.config/<host>/     │                     │                    │
      │  auth.json (mode 600) │                     │                    │
      │                        │                     │                    │
      │ ✓ ready — call API     │                     │                    │
      │   with access_token    │                     │                    │

Diagramme de séquence — PKCE Authorization Code Flow (RFC 8252 + 7636)

Mode optionnel pour CLI desktop capable d'ouvrir un browser ET d'écouter sur localhost.

┌────────────┐         ┌──────────────┐         ┌────────┐         ┌────────────┐
│ CLIENT CLI │         │ AUTH-FLOW    │         │ BROWSER│         │ USER       │
└─────┬──────┘         └──────┬───────┘         └───┬────┘         └─────┬──────┘
      │                       │                     │                    │
      │ ① generate code_      │                     │                    │
      │   verifier + state    │                     │                    │
      │   (via @mostajs/auth/ │                     │                    │
      │   lib/oauth-primitives)│                    │                    │
      │                       │                     │                    │
      │ ② start http://       │                     │                    │
      │   localhost:PORT      │                     │                    │
      │   listener            │                     │                    │
      │                       │                     │                    │
      │ ③ exec open BROWSER  │                     │                    │
      │   GET /oauth/authorize│                     │                    │
      │   ?response_type=code │                     │                    │
      │   &client_id=...      │                     │                    │
      │   &redirect_uri=      │                     │                    │
      │     localhost:PORT    │                     │                    │
      │   &state=...          │                     │                    │
      │   &code_challenge=... │                     │                    │
      │   &code_challenge_    │                     │                    │
      │    method=S256        │                     │                    │
      ├──────────────────────────────────────────────▶                   │
      │                       │                     │                    │
      │                       │ ④ user authenticates│                    │
      │                       │ ◀───────────────────┼────────────────────┤
      │                       │                     │                    │
      │                       │ ⑤ 302 redirect      │                    │
      │                       │   localhost:PORT/   │                    │
      │                       │   callback?         │                    │
      │                       │   code=XYZ          │                    │
      │                       │   &state=...        │                    │
      │                       │ ───────────────────▶│                    │
      │                       │                     │                    │
      │ ⑥ GET callback?      │                     │                    │
      │   code=XYZ            │                     │                    │
      │ ◀──────────────────────────────────────────│                    │
      │ verify state match    │                     │                    │
      │                       │                     │                    │
      │ ⑦ POST /oauth/token  │                     │                    │
      │   {code, code_verifier│                     │                    │
      │    redirect_uri,      │                     │                    │
      │    client_id}         │                     │                    │
      ├──────────────────────▶│                     │                    │
      │                       │ ⓐ verify SHA-256    │                    │
      │                       │   (code_verifier)   │                    │
      │                       │   == challenge      │                    │
      │                       │ ⓑ issueAccessToken  │                    │
      │ ⑧ TokenResponse       │                     │                    │
      │ ◀─────────────────────│                     │                    │
      │                       │                     │                    │
      │ ⑨ store.save(...)    │                     │                    │

Découplage strict

@mostajs/auth-flow ne dépend de NI :

  • @mostajs/api-keys (le host la branche via issueAccessToken)
  • @mostajs/auth (le host la branche via resolveUserSession)
  • @mostajs/payment (le host la branche via onAnonymousVisitor)
  • @mostajs/orm (option : consumer peut câbler une PendingStore ORM-backed, mais le module fournit aussi une in-memory)

Le module est autonome — quelques fetch, crypto.randomUUID(), des interfaces. C'est ce qui le rend ré-utilisable hors-Octonet pour de futurs projets.


RFC implementées

| Spec | Périmètre | Quand | |---|---|---| | RFC 8628 — OAuth 2.0 Device Authorization Grant | CLI / serveur / mobile / SSH / headless / context où on ne peut pas (ou ne veut pas) ouvrir un browser local | Toujours — défaut universel | | RFC 8252 — OAuth 2.0 for Native Apps + PKCE | Desktop CLI capable de spawn un browser ET d'écouter sur localhost:PORT | Optionnel — UX optimisée desktop | | RFC 7636 — PKCE (Proof Key for Code Exchange) | Composant cryptographique du PKCE flow | Inclus dans le PKCE flow |

Le module v0.1 livre les deux flows en parallèle (R1 du plan : pas de dette technique) — le client choisit selon son contexte d'exécution.


Quick start (côté client — CLI / SDK)

import { createDeviceFlow, createFsAuthStore } from '@mostajs/auth-flow/client'

const flow = createDeviceFlow({
  host: 'octonet',  // → ~/.config/octonet/auth.json (par défaut)

  authorizeEndpoint: 'https://octonet.cloud/api/v1/auth/device/authorize',
  tokenEndpoint:     'https://octonet.cloud/api/v1/auth/device/token',
  clientId:          'data-plug-cli',
  scope:             ['octonet.read'],

  onCodeIssued: ({ user_code, verification_uri, expires_in }) => {
    console.log(`\n  Open ${verification_uri}`)
    console.log(`  Enter code: ${user_code}`)
    console.log(`  Expires in ${Math.round(expires_in / 60)} min...\n`)
  },

  store: createFsAuthStore(),  // ~/.config/<host>/auth.json (XDG-compliant, mode 600)
})

const token = await flow.run()  // bootstrap le device flow + persiste le token
// → token.access_token est utilisable comme apikey

Si un token valide est déjà en ~/.config/octonet/auth.json, flow.run() le retourne immédiatement (pas de re-prompt).

Quick start (côté serveur — host : Octonet)

import { createDeviceFlowHandlers } from '@mostajs/auth-flow/server'
import { apiKeys } from '@mostajs/api-keys'   // host's choice
import { resolveSession } from './auth'        // host's choice

const handlers = createDeviceFlowHandlers({
  issueAccessToken: async ({ accountId, scopes, clientId, deviceCode }) => {
    const generated = await apiKeys.create({
      accountId,
      label: `auth-flow:${clientId}`,
      permissions: {
        scopes: scopes.reduce((m, s) => ({ ...m, [s]: '*' }), {}),
        metadata: { issuedVia: 'device-flow', clientId, deviceCode },
      },
      expiresAt: new Date(Date.now() + 365 * 86400_000),
    })
    return { token: generated.full, expiresIn: 365 * 86400 }
  },

  resolveUserSession: (req) => resolveSession(req),  // → { accountId } | null

  onAnonymousVisitor: ({ deviceCode, req, res }) => {
    // free / paid choice — host's call
    return res.redirect(`/signup?next=/device&code=${deviceCode}`)
  },

  pendingStore: createOrmPendingStore(),  // or createMemoryPendingStore() for tests

  defaultPage: 'vanilla',  // 'vanilla' = bundled HTML | 'none' = host serves its own
})

// Route wiring (Express / Fastify / Next.js — adapter your framework)
app.post('/api/v1/auth/device/authorize', handlers.authorize)
app.post('/api/v1/auth/device/token',     handlers.token)
app.get ('/device',                       handlers.devicePage)
app.post('/api/v1/auth/device/approve',   handlers.approve)

Méthodes de connexion — guide d'utilisation détaillé

Trois méthodes couvertes par le module, plus une situation cached (re-utilisation token déjà obtenu). À chaque méthode son contexte cible :

| Méthode | Contexte cible | RFC | UX | |---|---|---|---| | A — Token cached (no-prompt) | Toute exécution après une 1ʳᵉ connexion réussie | — | Zéro friction | | B — Device Flow | CLI headless / SSH / container / mobile / IoT / pas de browser local | 8628 | User ouvre browser sur autre device | | C — PKCE Localhost | Desktop CLI (macOS/Linux/Win avec browser) | 8252 + 7636 | Browser auto-ouvert + redirect localhost | | D — Custom store / mobile / serverless | App mobile, lambda, container immutable (pas de FS write) | — | Override AuthStore DI |

A — Token cached (le cas nominal après 1ʳᵉ connexion)

flow.run() est idempotent : il commence par store.load(), et si un token valide (non-expiré) y est trouvé, il est retourné immédiatement, sans aucun prompt.

import { createDeviceFlow, createFsAuthStore } from '@mostajs/auth-flow/client'

const flow = createDeviceFlow({
  host: 'octonet',
  authorizeEndpoint: 'https://octonet.cloud/api/v1/auth/device/authorize',
  tokenEndpoint:     'https://octonet.cloud/api/v1/auth/device/token',
  clientId: 'data-plug-cli',
  scope: ['octonet.read'],
  onCodeIssued: () => { /* jamais appelé si token cached */ },
  store: createFsAuthStore(),
})

const token = await flow.run()
// 1ʳᵉ exécution : déclenche le device flow, persiste, retourne le token.
// 2ᵉ exécution (jours plus tard) : lit ~/.config/octonet/auth.json, retourne directement.

Forcer un re-prompt (utile pour rotation manuelle) :

await flow.signOut()        // efface le fichier
const token = await flow.run()  // déclenche le flow complet
// — ou de manière équivalente :
const token = await flow.refresh()

B — Device Flow (RFC 8628) — SDK headless

Choix par défaut. Marche partout : SSH session, conteneur Docker, machine sans browser, mobile, IoT, CI/CD à approbation manuelle.

B.1 — Wire-up minimal côté CLI

import { createDeviceFlow, createFsAuthStore } from '@mostajs/auth-flow/client'

const flow = createDeviceFlow({
  host: 'octonet',
  authorizeEndpoint: 'https://octonet.cloud/api/v1/auth/device/authorize',
  tokenEndpoint:     'https://octonet.cloud/api/v1/auth/device/token',
  clientId: 'data-plug-cli',
  scope: ['octonet.read', 'octonet.storage.upload'],

  onCodeIssued: ({ user_code, verification_uri, verification_uri_complete, expires_in }) => {
    // Affichage CLI minimal — UX "Claude Code style"
    console.log('')
    console.log(`  Open the following URL in your browser:`)
    console.log(`    ${verification_uri}`)
    console.log('')
    console.log(`  Enter the code: ${user_code}`)
    console.log('')
    console.log(`  (Or scan: ${verification_uri_complete ?? '<not provided>'})`)
    console.log(`  Code expires in ${Math.round(expires_in / 60)} min...`)
    console.log('')
  },

  onSlowDown: () => console.warn('[auth] server asked us to slow polling'),

  store: createFsAuthStore(),
})

try {
  const token = await flow.run()
  console.log(`✓ Authenticated.`)
  // Utiliser token.access_token comme apikey dans les requêtes ultérieures
} catch (err) {
  console.error(`✗ Authentication failed:`, err)
  process.exit(1)
}

B.2 — Variantes d'affichage du user_code

| Contexte | UX recommandée | |---|---| | Terminal interactif | console.log avec couleurs ANSI + verification_uri cliquable | | GUI native (Electron) | dialog.showMessageBox({ message: user_code }) | | Mobile native | <QRCode value={verification_uri_complete} /> + <Text>{user_code}</Text> | | Web embed | popup léger ou banner de la web app | | Tests automatisés | onCodeIssued: (info) => mockApprove(info.user_code) |

B.3 — Enchaînement avec data-plug

C'est le cas d'usage canonique côté Octonet :

// data-plug/lib/auto-register.ts (étape 3 du plan @mostajs/auth-flow)
import { createDeviceFlow, createFsAuthStore } from '@mostajs/auth-flow/client'

export async function autoRegisterApiKey(): Promise<string> {
  const cached = await loadCached()
  if (cached?.access_token) return cached.access_token

  const flow = createDeviceFlow({
    host: 'octonet',
    authorizeEndpoint: process.env.MOSTA_NET_URL + '/api/v1/auth/device/authorize',
    tokenEndpoint:     process.env.MOSTA_NET_URL + '/api/v1/auth/device/token',
    clientId: 'data-plug-cli',
    scope: ['data-plug.full'],
    onCodeIssued: defaultTerminalRenderer,
    store: createFsAuthStore({ host: 'octonet' }),
  })

  const token = await flow.run()
  return token.access_token
}

C — PKCE Localhost (RFC 8252) — Desktop CLI

Activable comme alternative à B sur les machines de bureau capables d'ouvrir un browser ET d'écouter sur localhost. UX la plus fluide : zéro copier-coller du code.

import { createPkceFlow, createFsAuthStore } from '@mostajs/auth-flow/client'

const flow = createPkceFlow({
  host: 'octonet',
  authorizeEndpoint: 'https://octonet.cloud/api/v1/oauth/authorize',
  tokenEndpoint:     'https://octonet.cloud/api/v1/oauth/token',
  clientId: 'data-plug-desktop',
  scope: ['octonet.read'],
  callbackPort: 0,         // 0 = port dynamique (recommandé)
  callbackPath: '/callback',
  timeoutMs: 300_000,      // 5 min max d'attente du clic browser
  store: createFsAuthStore(),
})

// Sous le capot :
//   1. Génère code_verifier + state via @mostajs/auth/lib/oauth-primitives
//   2. Démarre HTTP listener sur localhost:<port>
//   3. Ouvre browser sur authorizeEndpoint avec ?redirect_uri=http://localhost:<port>/callback
//   4. Browser → user login → redirect → callback reçoit le code
//   5. POST tokenEndpoint avec code + code_verifier
//   6. Persiste via store
const token = await flow.run()

Quand préférer PKCE à Device Flow :

  • L'user est déjà devant un browser ouvert
  • Tu veux supprimer le copier-coller du user_code

Quand revenir à Device Flow :

  • SSH session (pas de browser local)
  • Container / serveur sans display
  • Le port localhost est bloqué par firewall
  • Tu n'as pas de manière de spawn open / xdg-open

C.1 — Fallback automatique conseillé

async function login(): Promise<string> {
  const config = { /* commun */ }
  try {
    const flow = createPkceFlow(config)
    return (await flow.run()).access_token
  } catch (err) {
    if (err instanceof Error && err.message.includes('no_browser')) {
      console.log('No browser available — falling back to device flow')
      const flow = createDeviceFlow(config)
      return (await flow.run()).access_token
    }
    throw err
  }
}

D — Override AuthStore (mobile / serverless / SecretManager)

Le default createFsAuthStore() écrit dans ~/.config/<host>/auth.json mode 0600. Inutilisable dans :

  • Lambda / Cloud Run (FS read-only ou éphémère)
  • Containers immutables (FS reset à chaque restart)
  • Mobile native (storage securisé OS-spécifique : Keychain iOS, KeyStore Android)

D.1 — Mobile : iOS Keychain via Capacitor

import { SecureStoragePlugin } from 'capacitor-secure-storage-plugin'
import { createDeviceFlow, type AuthStore, type AuthStoreRecord } from '@mostajs/auth-flow/client'

const keychainStore: AuthStore = {
  async load() {
    try {
      const v = await SecureStoragePlugin.get({ key: 'octonet:auth' })
      return JSON.parse(v.value) as AuthStoreRecord
    } catch { return null }
  },
  async save(record) {
    await SecureStoragePlugin.set({ key: 'octonet:auth', value: JSON.stringify(record) })
  },
  async clear() {
    await SecureStoragePlugin.remove({ key: 'octonet:auth' })
  },
}

const flow = createDeviceFlow({ /* ... */, store: keychainStore })

D.2 — Lambda : SecretManager AWS

import { SecretsManagerClient, GetSecretValueCommand, UpdateSecretCommand, DeleteSecretCommand } from '@aws-sdk/client-secrets-manager'

const sm = new SecretsManagerClient({})
const secretId = `octonet/auth/${process.env.LAMBDA_FUNCTION_NAME}`

const lambdaStore: AuthStore = {
  async load() {
    try {
      const r = await sm.send(new GetSecretValueCommand({ SecretId: secretId }))
      return JSON.parse(r.SecretString!) as AuthStoreRecord
    } catch (e: any) {
      if (e.name === 'ResourceNotFoundException') return null
      throw e
    }
  },
  async save(record) {
    await sm.send(new UpdateSecretCommand({ SecretId: secretId, SecretString: JSON.stringify(record) }))
  },
  async clear() {
    await sm.send(new DeleteSecretCommand({ SecretId: secretId, ForceDeleteWithoutRecovery: true }))
  },
}

D.3 — Tests : in-memory

import { createMemoryAuthStore } from '@mostajs/auth-flow/client'

const flow = createDeviceFlow({ /* ... */, store: createMemoryAuthStore() })

Côté serveur — wire-up détaillé par framework

Next.js App Router (Octonet typique)

// app/api/v1/auth/device/authorize/route.ts
import { handlers } from '@/lib/auth-flow-handlers'
export const POST = (req: Request) => handlers.authorize(req)

// app/api/v1/auth/device/token/route.ts
export const POST = (req: Request) => handlers.token(req)

// app/api/v1/auth/device/approve/route.ts
export const POST = (req: Request) => handlers.approve(req)

// app/device/page.tsx — délégué OU vanilla
export const dynamic = 'force-dynamic'
export default async function DevicePage({ searchParams }: { searchParams: { user_code?: string } }) {
  // Soit on délègue à handlers.devicePage (vanilla page)
  // Soit on rend notre propre page React :
  return <CustomDeviceConsentPage initialCode={searchParams.user_code} />
}

Fastify

import fastify from 'fastify'
import { handlers } from './auth-flow-handlers.js'

const app = fastify()

const wrap = (h: (r: Request) => Promise<Response>) => async (req: any, reply: any) => {
  const url = `${req.protocol}://${req.hostname}${req.url}`
  const headers = new Headers(req.headers)
  const body = req.body ? JSON.stringify(req.body) : undefined
  const webReq = new Request(url, { method: req.method, headers, body })
  const webResp = await h(webReq)
  reply.code(webResp.status)
  webResp.headers.forEach((v, k) => reply.header(k, v))
  reply.send(await webResp.text())
}

app.post('/api/v1/auth/device/authorize', wrap(handlers.authorize))
app.post('/api/v1/auth/device/token',     wrap(handlers.token))
app.get ('/device',                       wrap(handlers.devicePage))
app.post('/api/v1/auth/device/approve',   wrap(handlers.approve))

Express

Idem Fastify avec body-parser puis adapter reqRequest. Le module ne ship pas l'adaptateur — pattern bien connu (@vercel/node-like).

Branchement DI complet (Octonet de référence)

// lib/auth-flow-handlers.ts
import { createDeviceFlowHandlers, createOrmPendingStore } from '@mostajs/auth-flow/server'
import { getApiKeyService } from '@mostajs/api-keys'
import { auth } from '@/auth'  // NextAuth via @mostajs/auth
import { redirectToCheckout } from '@/lib/payment'  // @mostajs/payment

export const handlers = createDeviceFlowHandlers({
  baseUrl: process.env.PUBLIC_BASE_URL!,
  pendingStore: createOrmPendingStore({ orm: globalOrm }),

  // R2 — token via @mostajs/api-keys
  issueAccessToken: async ({ accountId, scopes, clientId, pendingId }) => {
    const apiKeys = await getApiKeyService()
    const generated = await apiKeys.create({
      accountId,
      label: `auth-flow:${clientId}`,
      permissions: {
        scopes: scopes.reduce((m, s) => ({ ...m, [s]: '*' as const }), {}),
        metadata: { issuedVia: 'device-flow', clientId, pendingId },
      },
      expiresAt: new Date(Date.now() + 365 * 86400_000),
    })
    return {
      access_token: generated.full,
      token_type: 'Bearer',
      expires_in: 365 * 86400,
      scope: scopes.join(' '),
    }
  },

  // R2 — session via @mostajs/auth
  resolveUserSession: async (req) => {
    const session = await auth(req)
    return session?.user?.accountId ? { accountId: session.user.accountId } : null
  },

  // R5 — anonymous → trial gratuit OU paid via @mostajs/payment
  onAnonymousVisitor: async ({ pendingId, req }) => {
    const url = new URL(req.url)
    const intent = url.searchParams.get('plan')  // 'free' ou 'paid'
    if (intent === 'paid') {
      // Stripe checkout via @mostajs/payment
      const checkoutUrl = await redirectToCheckout({ planId: 'pro-yearly', successPath: `/device?continue=${pendingId}` })
      return Response.redirect(checkoutUrl)
    }
    // Trial gratuit : sign-up direct + retour /device
    return Response.redirect(`${process.env.PUBLIC_BASE_URL}/signup?next=/device&pid=${pendingId}`)
  },

  expiresInSec: 1800,
  intervalSec: 5,
  defaultPage: 'vanilla',
})

Spec mappée sur le module

Endpoints (RFC 8628)

| Méthode | Path | RFC § | Module | |---|---|---|---| | POST | /device/authorize | §3.1 | handlers.authorize | | POST | /device/token | §3.4 | handlers.token | | GET | /device (page) | §3.2 (UX) | handlers.devicePage | | POST | /device/approve (interne, post-consent) | §3.3 | handlers.approve |

Endpoints PKCE (RFC 8252 + RFC 7636)

| Méthode | Path | RFC § | Module | |---|---|---|---| | GET | /oauth/authorize | RFC 6749 §4.1.1 | handlers.pkceAuthorize | | POST | /oauth/token | RFC 6749 §4.1.3 + RFC 7636 §4.5 | handlers.pkceToken |

Réponses normalisées

POST /device/authorize :

{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://octonet.cloud/device",
  "verification_uri_complete": "https://octonet.cloud/device?user_code=WDJB-MJHT",
  "expires_in": 1800,
  "interval": 5
}

POST /device/token :

  • En attente : { "error": "authorization_pending" } (RFC 8628 §3.5)
  • Trop fréquent : { "error": "slow_down" } (le client doit augmenter interval)
  • Approuvé : { "access_token": "...", "expires_in": 31536000, "token_type": "Bearer" }

Sécurité (R5 du plan)

| Vecteur | Mitigation | |---|---| | Brute-force user_code (8 chars) | Rate-limit côté /device/approve : max 5 tentatives/min/IP. Codes incluent un dash (WDJB-MJHT) pour réduire mistypes mais sans entropie supplémentaire. | | Replay device_code après consume | Marqué consumed_at à l'approbation ; tout polling ultérieur retourne expired_token. | | Phishing (faux site /device) | Le verification_uri doit être HTTPS + same-origin que tokenEndpoint. Documenté dans le warning au lancement CLI. | | Time-of-check (race) | pendingStore.consumeAtomic(deviceCode) garantit l'atomicité émetteur unique. | | TTL trop long | Default expires_in: 1800 (30 min). Le polling renvoie expired_token après. Configurable au boot. | | PKCE downgrade | Le serveur exige code_challenge_method=S256 ; rejette plain (RFC 7636 §4.2). | | State CSRF (PKCE) | state paramètre obligatoire, vérifié côté serveur. |


Persistence côté client

Format ~/.config/<host>/auth.json, mode 0600 :

{
  "version": 1,
  "host": "octonet",
  "endpoint": "https://octonet.cloud",
  "token": {
    "access_token": "ok_xxxxxxxxxxxx",
    "token_type": "Bearer",
    "expires_at": 1789234567,
    "scope": "octonet.read"
  },
  "issued_at": 1757234567,
  "client_id": "data-plug-cli"
}

Override via AuthStore DI pour serverless / containers immutables :

const flow = createDeviceFlow({
  ...
  store: {
    async load() { return await secureStorage.get('octonet:token') },
    async save(record) { await secureStorage.set('octonet:token', record) },
    async clear() { await secureStorage.delete('octonet:token') },
  }
})

Hors-périmètre v0.1 (décisions explicites — pas du vapor)

  • Refresh tokens — RFC 6749 §6 — postposé v0.2 si demande customer (la plupart des SDKs refont un device flow à expiration).
  • OIDC discovery (.well-known/openid-configuration) — postposé v0.2.
  • Dynamic client registration (RFC 7591) — out-of-scope, le clientId est convenu hors-bande pour v1.x.
  • Federated identity (Google login, etc., sur la page /device) — c'est au host de la câbler dans onAnonymousVisitor.
  • Audit log auth-flow — postposé v0.2 ; le host peut logguer via @mostajs/audit dans ses DI callbacks.

Modules amis (DI typique)

| Pour... | DI | Branchement Octonet | |---|---|---| | Émettre l'access_token | issueAccessToken({...}) | @mostajs/api-keys.create() | | Identifier l'user sur /device | resolveUserSession(req) | @mostajs/auth (NextAuth session) | | Free signup → trial / paid → checkout | onAnonymousVisitor({...}) | @mostajs/payment (Stripe / Chargily / Satim) | | Persister les device_code pending | pendingStore | @mostajs/orm (table auth_pending) | | Logger les events | (à la main dans les DI) | @mostajs/audit |


Plan d'implémentation

Voir docs/PLAN.md — étapes 1-4, ~5-6 jours dev senior pour atteindre la v0.1.0 livrable.


Release Notes

v0.1.0-alpha.3 — 2026-05-02 — Étape 2 PKCE (Session N+3) : pkce-flow.ts + pkce-handlers.ts + tests T3

Étape 2 du plan complétée — le module supporte maintenant les deux RFC en parallèle :

  • RFC 8628 (Device Flow) — alpha.1 client + alpha.2 server
  • RFC 8252 + 7636 (PKCE Authorization Code Flow) — alpha.3 client + server

Livré côté CLIENT — src/client/pkce-flow.ts (~280 lignes)

  • createPkceFlow() : run() + refresh() + signOut()
  • PKCE primitives inlined (zéro dep) : generateCodeVerifier (48 bytes base64url) + deriveCodeChallenge (SHA-256 base64url) + generateState (24 bytes)
  • Listener localhost sur port dynamique (callbackPort: 0) ou spécifié — node:http éphémère, écoute sur 127.0.0.1 uniquement
  • Browser open par défaut (open macOS / xdg-open Linux / start Win) ; override-able via openBrowser DI ; fallback : print URL si exec échoue
  • Anti-CSRF state mismatch : reject avec code: 'state_mismatch'
  • Timeout configurable (default 5 min) — listener clean shutdown si user n'ouvre pas
  • Token cached short-circuit identique au device flow (60s skew)
  • Page de succès HTML simple servie au browser après callback (UX "tu peux fermer cette fenêtre")

Livré côté SERVER — src/server/pkce-handlers.ts (~210 lignes)

  • createPkceHandlers() : pkceAuthorize + pkceToken
  • pkceAuthorize : GET /oauth/authorize — RFC 6749 §4.1.1 + RFC 7636 §4
    • code_challenge_method=S256 obligatoireplain rejeté (RFC 7636 §4.2 + §7.2)
    • State CSRF obligatoire ≥ 8 chars (RFC 6749 §10.12)
    • redirect_uri whitelist via DI validateRedirectUri({clientId, redirectUri})
    • User logué → consent implicite → pendingStore.insert(status='approved') → 302 redirect avec code + state
    • User anonymous → onAnonymousVisitor({pendingId, flow: 'pkce'}) (host gère signup/Stripe)
  • pkceToken : POST /oauth/token — RFC 6749 §4.1.3 + RFC 7636 §4.5/4.6
    • Vérifie grant_type='authorization_code', code, code_verifier, redirect_uri, client_id
    • PKCE verify : expected = SHA256(code_verifier), comparaison timingSafeEqual → si mismatch retourne invalid_request_pkce (= bad_verifier côté audit, attaque potentielle)
    • Atomicité : consumeAtomic après verify OK → impossible de réutiliser le code
    • issueAccessToken (DI) → TokenResponse

Tests T3 — 54 nouvelles assertions / ~3 s

test-scripts/test-pkce-handlers.ts (32 assertions, T3.1.A → T3.1.J)

  • Flow nominal user logué → 302 redirect avec code + state, pending status=approved accountId
  • Refus code_challenge_method=plaininvalid_request_pkce (RFC 7636 §4.2)
  • Refus state manquant ou trop court → invalid_request
  • Refus redirect_uri non whitelistée (DI validateRedirectUri retourne false) → invalid_request
  • Anonymous → onAnonymousVisitor invoqué avec flow=pkce + pendingId
  • pkceToken flow nominal verifier match challengeaccess_token issued via DI
  • Replay code après consumeinvalid_grant (anti double-issue)
  • Bad verifier (mauvais code_verifier) → invalid_request_pkce
  • Mismatch redirect_uri ou client_idinvalid_grant
  • grant_type incorrect ou champs manquants → unsupported_grant_type / invalid_request
  • Refus response_type != code

test-scripts/test-pkce-client.ts (22 assertions, T3.2.A → T3.2.G)

  • Flow nominal end-to-end : authorize URL bien formée (RFC params) + browser stub → callback localhost → exchange POST → access_token persisté
  • Token cached non-expiré → zéro HTTP, zéro browser
  • State mismatch (CSRF simulation) → throw state_mismatch
  • refresh() force browser malgré cached
  • signOut() vide le store
  • Token endpoint error → throw avec code propagé (invalid_request_pkce, etc.)
  • Callback ?error=access_denied → throw oauth_error

Suite agrégée auth-flow : 132 → 186 ✅

Bump 0.1.0-alpha.20.1.0-alpha.3.

Étape 2 close — Étape 3+ (cf. roadmap §3 N+4 et N+5)

@mostajs/auth-flow v0.1.0-alpha.3 est fonctionnellement complet côté module (device flow + PKCE, client + server, page vanilla). Reste pour atteindre 0.1.0 release stable :

  • Session N+4 : data-plug/lib/auto-register.ts + Octonet routes Next.js + page /device octonet-cloud
  • Session N+5 : onAnonymousVisitor branchement trial / Stripe via @mostajs/payment + tests E2E (Playwright)

v0.1.0-alpha.2 — 2026-05-02 — Étape 2 server (Session N+2) : pending-store + device-handlers + page vanilla

Deuxième moitié de l'Étape 2 du plan @mostajs/auth-flow (cf. roadmap §3 Session N+2 (b)). Implémente le device flow RFC 8628 côté serveur + page HTML vanilla.

Livré côté SERVER

src/server/pending-store.ts (~150 lignes)

  • createMemoryPendingStore() : Map + index multi-clés (deviceCode/userCode/state/id) + cleanup TTL via deleteExpired()
  • createOrmPendingStore({ orm, entityName? }) : adapté @mostajs/orm via repository(). Atomicité via updateMany conditionnés (WHERE status=X AND id=Y) — pattern standard SQL/Mongo, garantit single-emission même multi-instance.
  • approveAtomic({id, accountId}) : pendingapproved (ou false si déjà non-pending / expiré) — flip status='expired' au passage si TTL dépassé.
  • consumeAtomic(id) : approvedconsumed avec consumedAt=now (one-shot).

src/server/device-handlers.ts (~250 lignes)

  • createDeviceFlowHandlers() : authorize / token / devicePage / approve.
  • authorize : POST RFC 8628 §3.1 — génère device_code (32 bytes base64url) + user_code (8 chars alphabet sans ambiguïté no-O/0 no-I/1 format XXXX-XXXX), insert pending, retourne DeviceCodeResponse complet.
  • token : POST RFC 8628 §3.4 — polling, gère grant_type, mismatch client_id, pending/approved/denied/expired/consumed. consumeAtomic atomique → issueAccessToken (DI) → TokenResponse.
  • devicePage : GET — sert HTML vanilla par défaut, pageRenderer DI override-able, 'none' si le host sert sa propre page.
  • approve : POST avec brute-force tracker (5 attempts/min/IP → 429) ; user anonymous → délégué à onAnonymousVisitor DI (host gère signup/Stripe via @mostajs/payment).
  • Body parsing tolérant : application/x-www-form-urlencoded ET application/json.
  • Anti-enumeration : user_code inconnu retourne invalid_grant générique (pas 404).

src/web/device-page.ts (~110 lignes)

  • renderDevicePage(props) : HTML vanilla zero-dep, ~3kb
  • Branding configurable (serviceName, logoUrl, primaryColor)
  • securityNotice par défaut "Vérifie que cette URL correspond à ton service"
  • Form approve/deny via fetch POST x-www-form-urlencoded
  • Anti-XSS : escapeHtml sur tous les inputs templated

src/server/index.ts (modifié) : remplace les declare function placeholders par des re-exports export … from './device-handlers.js' / './pending-store.js'.

src/web/index.ts (modifié) : idem pour renderDevicePage.

Tests T2 — 74 nouvelles assertions / ~5 s

test-scripts/test-pending-store.ts (30 assertions, T2.1.A → T2.1.H)

  • Lookup multi-clés (deviceCode/userCode/state/id)
  • approveAtomic single-shot, refus si non-pending ou expiré
  • consumeAtomic flip approvedconsumed, refus replay (one-shot)
  • Race simulation : 2 consumeAtomic concurrents → exactement 1 réussi
  • deleteExpired clean les records dépassés + nettoie les indexes
  • id custom respecté à l'insert

test-scripts/test-device-handlers.ts (44 assertions, T2.2.A → T2.2.M)

  • authorizeDeviceCodeResponse complet, user_code XXXX-XXXX, pending insert
  • Refus client_id manquant → invalid_request 400
  • token pendingauthorization_pending
  • Flow nominal authorizeapprovetokenaccess_token issued via DI
  • Replay token endpoint après consumeexpired_token (anti double-issue)
  • grant_type incorrect → unsupported_grant_type
  • Mismatch client_idinvalid_grant
  • approve user anonymous → onAnonymousVisitor invoqué (host signup/Stripe)
  • Anti-enumeration : user_code inconnu → invalid_grant générique
  • Brute-force protection : 6ᵉ tentative wrong → 429
  • GET /device : HTML vanilla servi avec content-type: text/html, user_code pré-rempli
  • defaultPage=none404 (host gère sa propre page)
  • pageRenderer DI override
  • Body authorize en JSON aussi accepté

Suite agrégée auth-flow : 58 → 132 ✅

Bump 0.1.0-alpha.10.1.0-alpha.2.

Reste à faire pour clore Étape 2 (Session N+3 du roadmap)

  • src/client/pkce-flow.ts (RFC 8252 + 7636) avec listener localhost
  • src/server/pkce-handlers.ts (PKCE verify SHA-256 + state CSRF)
  • Tests T3 PKCE end-to-end

v0.1.0-alpha.1 — 2026-05-02 — Étape 2 client (Session N+1) : device-flow.ts + auth-store.ts

Première moitié de l'Étape 2 du plan @mostajs/auth-flow (cf. docs/PLAN.md + Entreprise/Octonet-as-Supabase/11-AUTOREGISTER-FLOW-ROADMAP.md §3.1) — implémentation du flow client RFC 8628 + persistance locale du token.

Livré côté CLIENT

src/client/auth-store.ts (~140 lignes)

  • createFsAuthStore() XDG-compliant :
    • Linux/macOS = $XDG_CONFIG_HOME/<host>/auth.json (fallback ~/.config/<host>/auth.json)
    • Windows = %APPDATA%/<host>/auth.json
  • Atomic write : .tmp.<random> + rename — pas de fichier partiel si crash en pleine écriture
  • Mode 0600 sur le fichier + 0700 sur le dir parent (mkdir recursive)
  • sanitizeHostName : neutralise les caractères filesystem (anti path-traversal), lowercase, max 64 chars, fallback 'mostajs' si vide
  • Tolère JSON corrompu et version inconnue (retourne null sans throw — caller refera le flow)
  • createMemoryAuthStore() pour les tests + reset() helper
  • resolveAuthFilePath() exporté pour les tests + outils tiers

src/client/device-flow.ts (~160 lignes)

  • createDeviceFlow() : run() + refresh() + signOut()
  • run() commence par store.load() — si token cached non-expiré (60s skew) → return direct, short-circuit zéro HTTP. Reconstruit expires_in = remaining seconds pour cohérence caller.
  • Sinon flow complet : POST /authorizeonCodeIssued(code, uri) → polling /token avec backoff respect interval + slow_down (+5s par occurrence + appel onSlowDown si fourni)
  • Erreurs RFC 8628 §3.5 traitées :
    • authorization_pending → continuer polling
    • slow_down → augmenter interval
    • access_denied → throw avec code: 'access_denied'
    • expired_token → throw avec code: 'expired_token'
  • À l'approbation : store.save(record) avec expires_at calculé en epoch ms
  • Body /authorize : application/x-www-form-urlencoded { client_id, scope }
  • Body /token : grant_type=urn:ietf:params:oauth:grant-type:device_code + device_code + client_id
  • maxPolls cap configurable (default = expires_in/interval + 5)

src/client/index.ts (modifié)

  • Remplace les declare function placeholders alpha.0 par des re-exports export … from './device-flow.js' / './auth-store.js'
  • Surface API publique identique au squelette alpha.0 — pas de breaking change

Tests T1 — 58 assertions / ~20 s

test-scripts/test-auth-store.ts (27 assertions, T1.1.A → T1.1.G)

  • sanitizeHostName : path traversal neutralisé, slashes filtrés, empty → fallback, troncature 64 chars, allowlist a-z0-9._-
  • resolveAuthFilePath : XDG_CONFIG_HOME honoré, fallback ~/.config, APPDATA Windows
  • Round-trip save/load + mode 0600 + dir 0700, .tmp orphelin absent post-rename
  • clear() supprime + idempotent ENOENT
  • JSON corrompu et version inconnue → null tolérant
  • Memory store + reset helper
  • Default host = 'mostajs' quand non fourni

test-scripts/test-device-flow-client.ts (31 assertions, T1.2.A → T1.2.J)

  • Flow nominal : authorize → polling pendingtokenstore.save invoqué
  • Cached token non-expiré : zéro HTTP fait
  • Cached expiré : flow complet redéclenché
  • slow_down : interval +5s entre 1ʳᵉ et 2ᵉ poll (mesuré ~7s, attendu 6.5-14s) + onSlowDown
  • access_denied : throw avec code, rien persisté
  • expired_token : throw avec code
  • refresh() force flow même avec cached
  • signOut() vide le store
  • Body /authorize bien formé (POST + client_id + scope joinable)
  • Body /token utilise grant_type RFC 8628 + device_code + client_id

test-scripts/run-tests.sh : runner agrégé adapté de mosta-auth (parse Resultats: X passed, Y failed).

Build : npx tsc --noEmit exit 0 + npm run build regénère dist/{client,server,types,web}/*.{js,d.ts} + fix-esm OK.

Reste à faire pour clore Étape 2

  • src/client/pkce-flow.ts (RFC 8252 + 7636) — Session N+3
  • src/server/{device-handlers,pending-store,pkce-handlers}.tsSession N+2
  • src/web/device-page.ts (HTML vanilla) — Session N+3
  • Mini-PR @mostajs/auth v3.0.1 : 10 nouveaux AuthEventKind device_flow.* + pkce.* (à faire en tête Session N+2, cf. roadmap §1 décision 9 + §3 phasing)

v0.1.0-alpha.0 — 2026-05-01 — Étape 1 : squelette + interfaces complètes

Premier commit du module @mostajs/auth-flow (cf. plan dans docs/PLAN.md).

Couvre l'Étape 1 du plan en 4 points :

  • package.json (alpha v0.1.0-alpha.0) + tsconfig.json + LICENSE + .gitignore
  • README.md complet avec spec RFC 8628 (Device Flow) + RFC 8252 (PKCE) + RFC 7636
    • Architecture avec découplage strict (zéro deps obligatoires)
    • 2 diagrammes de séquence ASCII (Device Flow + PKCE Flow)
    • Section "Méthodes de connexion — guide d'utilisation détaillé" avec 4 cas :
      • A — Token cached (no-prompt)
      • B — Device Flow (CLI headless / SSH / mobile / IoT)
      • C — PKCE Localhost (Desktop CLI)
      • D — Override AuthStore (mobile Keychain, Lambda SecretsManager, in-memory tests)
    • Section "Côté serveur" avec wire-up Next.js / Fastify / Express + DI complet Octonet
    • Spec mappée sur le module (endpoints + réponses RFC-conformes)
    • Sécurité : 7 vecteurs traités (brute-force user_code, replay, phishing, time-of-check, TTL, PKCE downgrade, state CSRF)
  • docs/PLAN.md : 4 étapes (~5-6 jours dev senior pour atteindre v0.1.0 livrable)
  • Interfaces TypeScript complètes SANS implémentation :
    • src/types/index.ts : DeviceCodeResponse, TokenResponse, OAuthErrorCode/Response, PendingRecord, PendingStatus, AuthStoreRecord, FlowKind
    • src/client/index.ts : AuthStore, DeviceFlowConfig/DeviceFlow + createDeviceFlow, PkceFlowConfig/PkceFlow + createPkceFlow, createFsAuthStore, createMemoryAuthStore
    • src/server/index.ts : IssueAccessToken, ResolveUserSession, OnAnonymousVisitor, PendingStore + consumeAtomic/approveAtomic, DeviceFlowHandlersConfig/Handlers + createDeviceFlowHandlers, PkceHandlersConfig/Handlers + createPkceHandlers, createMemoryPendingStore, createOrmPendingStore
    • src/web/index.ts : DevicePageProps, renderDevicePage

Décisions actées en Étape 1 (cf. docs/PLAN.md §contexte) :

  1. Nom : @mostajs/auth-flow (pas register-flow — couvre aussi le renouvellement)
  2. R1 — Pas de dette : Device Flow (RFC 8628) + PKCE (RFC 8252) en parallèle dès v0.1
  3. R2 — Token via @mostajs/api-keys (DI issueAccessToken) — chaque module fait son travail
  4. R3~/.config/<host>/auth.json XDG-compliant, host paramétrable au boot (default 'mostajs')
  5. R4 — Page /device HTML vanilla zero-dep + override-able (pageRenderer DI)
  6. R5 — Free vs paid délégué au host via onAnonymousVisitor (Octonet branche @mostajs/payment Stripe checkout ou signup direct)

Audits préalables ✅ :

  • @mostajs/[email protected] : metadata accepte clientId + issuedVia + pendingId
  • @mostajs/[email protected] : multi-provider (Stripe/Satim/CIB/Chargily/PayPal) prêt
  • @mostajs/[email protected]+ : lib/oauth-primitives expose generateCodeVerifier, deriveCodeChallenge, generateState — auth-flow l'importera à l'Étape 2

Build clean : tsc --noEmit + npm run build sans erreur, dist/{client,server,types,web} générés.


License

AGPL-3.0-or-later + commercial — [email protected].