qumra-pos-auth
v0.1.2
Published
Offline-capable identity for POS/apps: OAuth token vault with single-flight rotating refresh, local PIN (argon2id), Ed25519 offline activation, and an online/offline-degraded/offline-activated mode + grace-lock state machine. Zero dependency on any storag
Maintainers
Readme
qumra-pos-auth
Offline-capable identity for POS and local-first apps. Pure-JS, cross-platform (React Native, Web, Desktop, Node), and completely independent of any storage layer — you wire it to your data/sync code through one callback.
Install
npm i qumra-pos-auth
# or
yarn add qumra-pos-auth
# or
pnpm add qumra-pos-authRuntime deps are only @noble/hashes (argon2id) and @noble/curves (Ed25519) — both
pure-JS, no native build on any platform.
What it does
- Three operating modes —
online(valid tokens, syncing) ·offline-degraded(was online, token expired / no network, still runs on a local session) ·offline-activated(an Ed25519-signed activation key, verified locally, server never contacted) ·unactivated. active → grace → lockedstate machine — data is never erased; the worst case is that new sales lock. Grace still sells (with a warning); recovery = any successful online contact.- Tokens — OAuth token vault with single-flight rotating refresh (15-min access /
30-day refresh by default).
getValidToken()never throws —nullmeans pause, not logout. - Local cashier PIN — argon2id verified against a synced hash (same code online & offline), with per-cashier lockout.
- Offline activation — parses
payload.signature, verifies the Ed25519 signature against a pinned public key, then device + expiry.
Quick start — the library takes a token from you
This package has zero endpoint code. You do login/refresh (REST, GraphQL, anything)
and hand it the tokens through the AuthApi port. It never knows your server.
import { createAuth, type AuthApi } from "qumra-pos-auth";
// YOU own the network. The library only calls these two methods.
const api: AuthApi = {
async login({ username, password }) {
const t = await myBackend.login(username, password); // your REST/GraphQL call
return { accessToken: t.access, refreshToken: t.refresh, expiresInSec: 900, refreshExpiresInSec: 2592000 };
},
async refresh(refreshToken) {
const t = await myBackend.refresh(refreshToken); // your call
return { accessToken: t.access, refreshToken: t.refresh, expiresInSec: 900, refreshExpiresInSec: 2592000 };
},
};
const auth = createAuth({ api, vault, directory, activationPublicKey });
await auth.service.init();
await auth.service.onlineLogin({ username, password });
await auth.service.cashierLogin("cashier-1", "1234");
auth.service.onStatusChange((s) => renderBar(s)); // { mode, access, cashier, ... }Even simpler — if you manage tokens 100% outside and don't need the vault/refresh
logic, skip the auth token layer entirely and just feed your token to the sync engine:
new SyncEngine(storage, { getAuthToken: () => myToken }).
The one seam to your storage/sync
qumra-pos-auth never imports your storage. Wire them in the app with a single callback.
Since sync engines usually poll a synchronous token getter, use getAccessTokenSync
(an in-memory cache kept warm by the async refresh loop):
// e.g. with qumra-pos-storage's SyncEngine:
new SyncEngine(storage, { getAuthToken: () => auth.getAccessTokenSync() });getAccessTokenSync(): string | null— synchronous, no I/O.null⇒ sync pauses.getValidToken(): Promise<string | null>— async variant (triggers refresh if needed).
Ports you implement (per platform)
Everything I/O is an injectable port, so the same core runs everywhere:
AuthVault— secure persistence. Back it with ElectronsafeStorage, browser IndexedDB, or RN Keychain. (saveTokens/loadTokens,…Session,…Activation,…Meta.)CashierDirectory—listCashiers(); return the synced cashier rows (id, name,pinHash, active) from your storage.AuthApi— you implementlogin/refreshwith your own fetch/GraphQL. The library never knows your endpoint; it just receives the tokens you return.PinHasher/SignatureVerifier— default to the bundledNoblePinHasher(argon2id, PHC format) andNobleSignatureVerifier(Ed25519) fromqumra-pos-auth/crypto; swap for a native impl on RN if you want.
Error semantics for your AuthApi
Throw the right error so the state machine behaves correctly:
- Network / server-down / 5xx →
throw new AuthNetworkError(...)— retryable, keeps the tokens, sync just pauses. - Rejected (bad credentials / refresh revoked) →
throw new AuthRejectedError(...)— permanent, clears the tokens.
Both are exported from qumra-pos-auth.
Publishing
Ships dual ESM + CJS + .d.ts (built with tsup). Crypto is also available at the
qumra-pos-auth/crypto subpath. To publish under a different name, change name in
package.json.
MIT.
