@softwarepatterns/am
v0.4.0
Published
Auth client SDK for AccountMaker (Am)
Maintainers
Readme
@softwarepatterns/am
Authentication client SDK for AccountMaker (Am).
This package provides a small, stable client for interacting with the AccountMaker authentication API. It is designed with explicit support for both ESM and CommonJS consumers.
Features
- Sessions are created through
Amauth and restore methods - Automatic access-token refresh until the session requires a hard reload
- TypeScript-first API with bundled
.d.ts - Explicit error modeling via RFC 9457 Problem Details
- Typed auth lifecycle events
- No runtime dependencies or side effects on import
- ESM and CommonJS support
Requirements
- Node.js ≥ 18
- A runtime with a global
fetchimplementation (Node 18+, Bun, Deno, or a customfetchFn)
Installation
npm install @softwarepatterns/amImporting
ESM
import { Am } from "@softwarepatterns/am";CommonJS
const { Am } = require("@softwarepatterns/am");Basic usage
Create a client
import { Am } from "@softwarepatterns/am";
const am = new Am();Examples live in examples/.
Authentication flows
Sign in with email and password
const session = await am.signIn({
clientId: "your-client-id",
email: "[email protected]",
password: "password"
});Sign up (register) a new user with email and password
const session = await am.signUp({
clientId: "your-client-id",
email: "[email protected]",
password: "password"
});Accept an invite from an email link
const session = await am.acceptInvite({
clientId: "your-client-id",
token: "token-from-email",
});Sign in with a token from an email link
const session = await am.signInWithToken("token-from-email");Sessions
A session represents an authenticated user and handles token refresh automatically.
// Automatically adds Authorization header, will refresh tokens as needed,
const res = await session.fetch("https://yourdomain.com/some/protected/resource");You can also use the access token yourself.
if (session.isExpired()) {
await session.refresh();
}
await fetch("https://yourdomain.com/your/own/protected/api", {
headers: {
Authorization: `Bearer ${session.tokens.accessToken}`
}
});Sessions are produced by Am methods such as signIn(), signUp(),
signInWithToken(), acceptInvite(), createSession(), and
restoreSession().
Auth events
Subscribe to auth lifecycle events with am.on(...).
const stopSignedIn = am.on("signedIn", (session) => {
console.log("signedIn", session.profile.id);
});
const stopAuthLost = am.on("authLost", (error) => {
console.log("authLost", error.status, error.title);
});
const stopProfileUpdated = am.on("profileUpdated", (profile) => {
console.log("profileUpdated", profile.lastUpdatedAt);
});
const stopTokensUpdated = am.on("tokensUpdated", (tokens) => {
console.log("tokensUpdated", tokens.expiresAt);
});
const stopReloadRequired = am.on("reloadRequired", () => {
window.location.reload();
});Event meaning:
signedIn: a session was established throughAmtokensUpdated: session tokens rotatedprofileUpdated: session profile refreshedauthLost: auth was lost but the runtime is still recoverablereloadRequired: the current session can continue only after a hard reload
After reloadRequired, the current runtime should hard-navigate before doing
more session work. authLost does not carry that requirement.
Your own services can validate the access token using AM's public keys.
import * as jose from 'jose';
// Will auto fetch, cache, and rotate keys as needed
const jwksUrl = new URL('https://auth.yourdomain.com/.well-known/jwks.json');
// if not using a custom auth domain:
// const jwksUrl = new URL('https://api.accountmaker.com/.well-known/jwks.json?client_id=your-client-id');
const JWKS = jose.createRemoteJWKSet(jwksUrl);
export const validateAccessToken = async (token: string) => {
const { payload } = await jose.jwtVerify(token, JWKS);
console.log('Account Id:', payload.acc);
console.log('User Id:', payload.uid);
console.log('User Account Role:', payload.role);
}
By default, tokens are saved in memory only.
const am = new Am({
storage: null // disable storage, default is in-memory
});Set to "localStorage" to persist sessions across reloads.
const am = new Am({
storage: "localStorage"
});
const session = am.restoreSession();Or implement your own storage mechanism by implementing the Storage interface.
Error handling
All API errors throw as AuthError.
import { AuthError } from "@softwarepatterns/am";
try {
await am.signIn({ email, password });
} catch (err) {
if (err instanceof AuthError) {
console.log(err.status);
console.log(err.title);
console.log(err.detail);
}
}Errors follow RFC 9457 Problem Details format (application/problem+json).
Unknown or unsupported error responses are converted into a generic problem shape without exposing raw response bodies. All non-HTTP errors (such as network or parsing errors) are left alone to bubble up.
Custom fetch
For mocking or for custom fetch implementations, you can provide your own.
const am = new Am({
fetchFn: fetch,
});Stability guarantees
The following are guaranteed:
- Stable ESM and CommonJS entry points
- Stable TypeScript types for exported symbols
- Compatibility with Node 18, 20, and 22
This package follows semantic versioning.
0.xreleases may introduce breaking changes1.0.0will mark a frozen public API
Development and Testing
Prerequisites
Integration tests require credentials stored in the repository root .env.
The encrypted version .env.enc is committed at the repository root.
Decrypting credentials
# From the repository root
sops -d .env.enc > .envRunning tests
# Unit tests
bun run --filter @softwarepatterns/am test:unit
# Integration tests (requires .env)
bun run --filter @softwarepatterns/am test:integrationThe package integration script reads credentials from the repository root
.env file.
Re-encrypting after changes
# From the repository root
sops -e .env > .env.encLicense
MIT
