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

@astratra/native

v0.1.0

Published

Mobile plumbing without the mobile runtime: secure token session with an injected keystore, a biometric gate, native push registration and foreground watching, and a checkout round-trip that survives a missing native module. Adapters injected, so it tests

Readme

@astratra/native

La plomberie mobile, sans le moteur mobile : session sécurisée, verrou biométrique, notifications natives, retour de paiement.

Le package ne charge ni expo-secure-store, ni expo-local-authentication, ni expo-web-browser. Il les reçoit. Même règle que @astratra/notify avec son transport, et même bénéfice : tout ce qui suit se teste en Node, sans simulateur ni build natif.

Aucune dépendance à l'exécution.


Session

Le trousseau du téléphone est un module natif : l'importer ici ferait entrer un build natif dans chaque test, et rendrait le package inutilisable partout où le module manque — un aperçu navigateur, un client de développement compilé avant son installation.

Le trousseau est donc injecté. Le contrat est celui d'expo-secure-store, noms de méthodes compris — le renommer aurait obligé chaque appelant à écrire une enveloppe pour la seule implémentation que tout le monde utilise.

import * as SecureStore from 'expo-secure-store';
import { createSecureSession } from '@astratra/native';

const session = createSecureSession({ keystore: SecureStore, namespace: 'acme' });

await session.save({ accessToken, refreshToken });
const token = await session.getAccessToken();

Deux décisions, chacune née d'un vrai défaut.

Le jeton d'accès est gardé en mémoire. Lire le trousseau est un aller-retour natif, et chaque requête a besoin du jeton : un écran de liste et ses douze requêtes parallèles faisaient douze lectures du Keychain.

Effacer emporte le drapeau biométrique. Ce drapeau dit « cette personne a choisi de déverrouiller par son visage » — il appartient à la session qui l'a activé. Laissé derrière à la déconnexion, il reste actif pour le compte suivant sur le même appareil, un inconnu compris.

Un trousseau peut échouer : Keychain verrouillé, permission révoquée, stockage navigateur désactivé. Une lecture qui lève est répondue « pas de session » — la personne se reconnecte, ce qui est mauvais, quand un plantage au lancement est pire.

Deux adaptateurs accompagnent le contrat : createMemoryKeystore() pour les tests, createWebKeystore(localStorage) pour l'aperçu navigateur — jamais pour un build web livré : localStorage n'est pas un trousseau, n'importe quel script de la page le lit.

Verrou biométrique

Trois faits, et une règle par fait.

import * as LocalAuthentication from 'expo-local-authentication';
import { createBiometricGate } from '@astratra/native';

const gate = createBiometricGate({
  keystore: SecureStore,
  authenticator: LocalAuthentication,
  namespace: 'acme',
  promptMessage: 'Acme'
});

const { supported, enabled } = await gate.read();
await gate.enable();          // demande, PUIS retient
const unlocked = await gate.confirm();

« Pris en charge » demande les deux faits de l'appareil — un capteur, et une empreinte enregistrée. Un téléphone avec capteur et rien d'enregistré affiche l'invite et refuse aussitôt : l'utilisateur y lit une fonction cassée.

Activer demande d'abord, retient ensuite. L'inverse laisserait l'application prétendre un déverrouillage que personne n'a accordé.

Un drapeau ne survit pas à l'empreinte qu'il désigne. Si l'empreinte a été retirée du téléphone depuis, enabled retombe à faux tout seul.

confirm() ne propose rien quand le verrou n'a pas été activé — une invite biométrique que personne n'a demandée est ce qui fait signaler une application comme hameçonnage.

Rien ne lève ici. Un capteur peut être occupé, une permission révoquée entre deux écrans : l'appelant reçoit un état à afficher, jamais une exception à rattraper.

Notifications

Quand demander

L'invite système ne se lève qu'une fois. La lever au lancement, avant que la personne sache ce que l'application envoie, transforme un « non » réflexe en refus définitif : le système ne redemande jamais, et le seul retour possible est un détour par les réglages que presque personne ne fait.

decideRegistrationAction({ explicit: false, permission: 'undetermined' }); // 'none'
decideRegistrationAction({ explicit: true,  permission: 'undetermined' }); // 'request'
decideRegistrationAction({ explicit: true,  permission: 'denied' });       // 'open-settings'
decideRegistrationAction({ explicit: false, permission: 'granted' });      // 'register'

Où mène un appui

Une notification porte une route venue du serveur. La suivre à l'aveugle ouvre des écrans que le destinataire n'a pas à voir : cette route est une donnée du réseau, et les rôles changent entre l'envoi et l'appui.

Les règles se déclarent par autorisation, jamais par exclusion — une liste de routes interdites admet en silence chaque écran ajouté ensuite et oublié.

const router = createNotificationRouter({
  fallback: '/notifications',
  routes: [
    { pattern: /^\/orders$/, allow: (role) => role === 'seller' },
    { pattern: /^\/billing$/, allow: (role) => role === 'owner', to: '/billing/overview' }
  ],
  actions: { REFUND: (payload) => `/orders/${payload.orderId}/refund` }
});

router.resolve('/orders?from=push', 'courier'); // '/notifications'

La veille au premier plan

Le push traite l'application fermée. Une application ouverte ne reçoit rien : la cloche restait figée jusqu'à ce qu'on quitte l'écran et qu'on y revienne.

if (shouldPoll(appState, Boolean(user)) ) {
  const items = await fetchNotifications();
  for (const item of freshItems(items, lastSeen)) banner(item);
  lastSeen = nextStamp(items, lastSeen);
}

Le premier relevé ne lève rien : au démarrage, tout l'historique non lu « vient d'arriver » du point de vue du téléphone, et vingt bannières d'un coup font couper les notifications pour de bon. Le repère ne recule jamais, sinon la même notification re-bannère au relevé suivant. Une date illisible n'est pas maintenant — la traiter comme l'instant présent bannèrerait tout l'arriéré.

L'interrupteur des réglages

const controller = createPushSettingsController(operations, setSnapshot);
const screen = controller.activate();
// à la sortie de l'écran :
screen.dispose();

Une réponse réseau survit à l'écran qui l'a demandée. Appuyez, quittez : la réponse arrive sur un écran disparu et écrase l'état de celui qui a suivi. Chaque activation reçoit une génération ; une génération qui n'est plus la courante ne publie rien.

L'état n'est jamais déduit de l'action — il est relu de l'appareil ensuite. Supposer « l'activation a réussi, donc c'est actif » est ce qui laisse un interrupteur allumé au-dessus de notifications éteintes.

Retour de paiement

const openCheckout = createCheckoutOpener({
  linking: Linking,
  loadBrowser: () => (requireOptionalNativeModule('ExpoWebBrowser') ? require('expo-web-browser') : null)
});

const returned = await openCheckout(url, 'acme://paid');

Le module natif peut ne pas être là. Un client de développement compilé avant son installation ne le contient pas, et le seul fait de l'importer lève — une erreur qu'un try rattrape, mais que l'écran rouge affiche quand même. Son absence est un cas prévu : le navigateur du téléphone prend le relais, le paiement aboutit, le retour est juste moins fluide.

La passerelle renvoie vers le site, pas vers l'application. Sans lien de retour, la personne reste échouée sur une page web, son achat quelque part derrière.

true signifie la personne est revenue par le lien de retour, jamais « le paiement a réussi ». Seul le serveur le sait, et un client qui en décide seul finit par offrir quelque chose.

Effet de verre

resolveGlassMode({ platform, apiAvailable, effectAvailable }); // 'native' | 'fallback'

Décidé une fois, pour que toutes les surfaces répondent pareil : un en-tête qui floute au-dessus d'une carte qui ne floute pas est pire que ni l'un ni l'autre.