JSPM

@bhudevswayam/blueprint-engine

1.1.3
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 33
  • Score
    100M100P100Q83363F
  • License MIT

Opinionated architectural framework for Node.js — multi-tenant auth, MFA, OAuth, and RBAC as a single import.

Package Exports

  • @bhudevswayam/blueprint-engine
  • @bhudevswayam/blueprint-engine/src/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@bhudevswayam/blueprint-engine) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

@bhudevswayam/blueprint-engine

An opinionated architectural framework for Node.js. Multi-tenant auth, MFA, OAuth, RBAC, and rate limiting — all wired into your Express app with one blueprintEngine(secret) call.

⚠️ This library is multi-tenant by design

Every User, Tenant, RefreshToken, and LoginAttempt document carries a tenantId. Every protected request must include an x-tenant-id header that matches the tenant of the authenticated user — protect returns 403 on mismatch and setTenant returns 400 when the header is missing.

If your client does not send x-tenant-id on every authenticated request, things will break:

  • Any route guarded by setTenant returns 400 Missing x-tenant-id header.
  • Any route guarded by protect returns 403 Tenant mismatch if the header value doesn't equal user.tenantId (unless the user's role is superadmin).
  • Refresh token rotation, MFA challenge, and logout all fail with 403 if the header is wrong or missing.

The tenant is created automatically at /register (derived from the user's name) and encoded into the JWT, so after login your client can read tenantId from the decoded access token and send it on every subsequent request. Single-tenant apps: hardcode one value client-side. There is no "turn multi-tenancy off" flag.

Install

npm install @bhudevswayam/blueprint-engine

Peer dependencies (install separately). dotenv is also needed so the app can read MONGO_URI, JWT_SECRET, and the OAuth vars from a .env file:

npm install express mongoose dotenv
  • express@^5.2.1
  • mongoose@^8.5.1
  • dotenv (any recent version)

Requires Node.js 18 or newer.

Quickstart

const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();
const blueprintEngine = require('@bhudevswayam/blueprint-engine');

const app = express();
app.use(express.json());

if (!process.env.MONGO_URI) {
  console.error('MONGO_URI is missing in demo/.env');
  process.exit(1);
}

mongoose.set('bufferCommands', false);

mongoose.connect(process.env.MONGO_URI, { serverSelectionTimeoutMS: 5000 })
  .then(() => console.log('MongoDB Connected'))
  .catch(err => {
    console.error('MongoDB Error:', err.message);
    process.exit(1);
  });

const engine = blueprintEngine(process.env.JWT_SECRET || 'dev-secret', {
  hooks: {
    onRegister:      async (user) => console.log('[HOOK] Registered:', user.email),
    onLogin:         async (user) => console.log('[HOOK] Login:', user.email),
    onLogout:        async (user) => console.log('[HOOK] Logout:', user.email),
    onPasswordReset: async (user, token) => console.log('[HOOK] Password reset token:', token),
  },
  oauth: {
    google: {
      clientID:        process.env.GOOGLE_CLIENT_ID,
      clientSecret:    process.env.GOOGLE_CLIENT_SECRET,
      callbackURL:     process.env.GOOGLE_CALLBACK_URL,
      successRedirect: process.env.OAUTH_SUCCESS_REDIRECT,
    }
  }
});

// OAuth provider redirects (and their callbacks) carry no x-tenant-id header,
// so skip the tenant guard for those paths.
const OAUTH_PATHS = ['/api/auth/google', '/api/auth/facebook', '/api/auth/twitter'];
app.use((req, res, next) => {
  if (OAUTH_PATHS.some(p => req.path.startsWith(p))) return next();
  return engine.setTenant(req, res, next);
});
app.use('/api/auth', engine.auth);

app.get('/health', (req, res) => res.json({
  status: 'ok',
  tenantId: req.tenantId
}));

// Error handler — must be last (4-arg signature)
app.use((err, req, res, next) => {
  console.error('[ERROR]', err.message);
  res.status(err.statusCode || 500).json({
    success: false,
    message: err.message || 'Internal server error'
  });
});

app.listen(4000, () => {
  console.log('Demo running on http://localhost:4000');
  console.log('');
  console.log('Auth endpoints:');
  console.log('  POST /api/auth/register');
  console.log('  POST /api/auth/login');
  console.log('  POST /api/auth/refresh');
  console.log('  POST /api/auth/logout');
  console.log('  POST /api/auth/forgot-password');
  console.log('  POST /api/auth/reset-password');
  console.log('');
  console.log('MFA endpoints:');
  console.log('  GET  /api/auth/mfa/setup');
  console.log('  POST /api/auth/mfa/verify-setup');
  console.log('  POST /api/auth/mfa/challenge');
  console.log('  POST /api/auth/mfa/disable');
  console.log('');
  console.log('Other:');
  console.log('  GET  /health');
  console.log('');
  console.log('Required header: x-tenant-id: tenant-001');
});

That's a fully functional backend with register, login, refresh, logout, password reset, MFA, and Google OAuth endpoints — all mounted under /api/auth.

.env structure

Create a .env file next to app.js with the following keys:

PORT=4000
MONGO_URI=mongodb://localhost:27017/tempdbauth
JWT_SECRET=supersecret_jwt_key_here_change_me
JWT_EXPIRES_IN=7d
SUPER_ADMIN_EMAIL=super@admin.com
SUPER_ADMIN_PWD=change_me
SUPER_TENANT_ID=global

GOOGLE_CLIENT_ID=<your-google-client-id>
GOOGLE_CLIENT_SECRET=<your-google-client-secret>
GOOGLE_CALLBACK_URL=http://localhost:4000/api/auth/google/callback
OAUTH_SUCCESS_REDIRECT=http://localhost:3000/

GOOGLE_CALLBACK_URL must exactly match a URI listed under Authorized redirect URIs in your Google Cloud Console OAuth client — otherwise Google returns Error 400: redirect_uri_mismatch. OAUTH_SUCCESS_REDIRECT is where the user lands after successful auth (typically your frontend SPA); the library appends ?accessToken=...&refreshToken=... to it.

API reference

blueprintEngine(jwtSecret, options?)

Returns { auth, setTenant, protect, authorize }.

Param Type Required Description
jwtSecret string yes Secret used to sign access and refresh tokens. Must be long and random in production.
options.hooks object no Optional lifecycle callbacks — see below.
options.oauth.google object no Google OAuth config. If absent, /api/auth/google is not mounted.
options.mfaSecret string no AES key used to encrypt TOTP secrets before storing them. Falls back to jwtSecret if omitted.

Lifecycle hooks

All hooks are optional. They receive the affected user document.

blueprintEngine(secret, {
  hooks: {
    onRegister:      async (user) => {},
    onLogin:         async (user) => {},
    onLogout:        async (user) => {},
    onMFAChallenge:  async (user, method, otpCode) => {
      // Called when email-method MFA is selected. `otpCode` is the 6-digit
      // code — deliver it however you want (email, SMS, Slack).
    },
    onPasswordReset: async (user, rawResetToken) => {
      // Send rawResetToken to the user via email, link back to your reset page.
    },
  },
});

Endpoints

All endpoints are mounted under the router returned as be.auth.

Method Path Auth Description
POST /register public Create an account. Creates a new tenant automatically. Returns user + access token.
POST /login public Authenticate. Returns { accessToken, refreshToken } — or { mfaPendingToken } if MFA is enabled.
POST /refresh public (needs refresh token in body) Rotate refresh token, issue new pair.
POST /logout bearer token Blacklist the access token and delete the refresh token.
POST /forgot-password public Generate a reset token, fire onPasswordReset hook. Always returns 200 to avoid leaking which emails exist.
POST /reset-password public (needs reset token) Consume reset token, set new password. Body: { token, password }.
POST /change-password bearer token Authenticated password change. Body: { currentPassword, newPassword }.
GET /mfa/setup bearer token Generate TOTP secret + QR code URI. MFA is not yet enabled.
POST /mfa/verify-setup bearer token Submit the first TOTP code to enable MFA. Returns 8 one-time backup codes.
POST /mfa/challenge mfaPendingToken in body Second factor verification. Accepts TOTP, email OTP, or a backup code. Returns access + refresh tokens.
POST /mfa/disable bearer token Disable MFA after verifying current TOTP or backup code.
GET /google public Initiate Google OAuth (mounted only if options.oauth.google is configured).
GET /google/callback Google redirects here. Issues tokens, redirects to successRedirect with accessToken + refreshToken as query params.

Request / response shapes

POST /register

Body: { name, email, password, role?, ...extraProfileFields }
201 → { id, name, email, role, tenantId, token }

name is slugified into the tenant ID: john_doe_<Date.now()>. Role defaults to "user". Any extra body fields are stored on the User document as-is.

POST /login

Body: { email, password }
200 (no MFA) → { success: true, data: { id, name, email, role, tenantId, accessToken, refreshToken } }
200 (MFA enabled) → { success: true, data: { mfaPending: true, mfaPendingToken, method } }

Rate-limited: 10 attempts per tenantId + IP per 15 minutes. Brute-force lockout: 5 consecutive failures per tenantId:email locks the account for 15 minutes.

POST /refresh

Body: { refreshToken }
200 → { success: true, data: { accessToken, refreshToken } }   // new pair; old refresh is deleted

POST /mfa/challenge

Body: { mfaPendingToken, code }
200 → { success: true, data: { id, name, email, role, tenantId, accessToken, refreshToken } }

Middleware

protect

Verifies the Authorization: Bearer <token> header, decodes the JWT, loads the user by ID, checks against the blacklist, and enforces tenant match (if x-tenant-id header is present it must equal user.tenantId, unless role is superadmin). Attaches the full Mongoose user document to req.user.

app.get('/private', be.protect, handler);

authorize(...allowedRoles)

Factory. Returns a middleware that 403s if req.user.role isn't in the list. Must come after protect.

app.get('/admin', be.protect, be.authorize('admin', 'superadmin'), handler);

setTenant

Requires the x-tenant-id header. Returns 400 if missing, otherwise sets req.tenantId. Safe to mount globally — OAuth provider paths (/auth/google, /auth/facebook, /auth/twitter and their callbacks) are automatically exempt because top-level browser redirects from those providers can't carry custom headers. Register and login do require the header (your client should send it on every request, including those).

// Global — recommended
app.use(engine.setTenant);
app.use('/api/auth', engine.auth);

// Or scoped to specific routers if you prefer
app.use('/api/bookings', engine.setTenant, bookingRouter);

Multi-tenancy

Every User and resource document carries a tenantId. Register creates a tenant implicitly from the user's name (name_timestamp), so a fresh signup produces a fresh isolated tenant with that user as its first member.

  • Clients authenticate by sending Authorization: Bearer <token> and x-tenant-id: <theirTenant> on every protected request.
  • protect enforces the tenant match — sending another tenant's ID returns 403.
  • superadmin role bypasses the tenant check.

Schemas

User

{ name, email (unique), password (bcrypt, optional for OAuth), role (enum), tenantId,
  phoneNo, addressLine1/2, city, state, zipCode,
  provider (google/facebook/twitter), providerId,
  passwordResetToken/Expires,
  mfa: { enabled, secret (AES-encrypted), backupCodes (bcrypt-hashed), method },
  otpCode/otpExpires }

Tenant

{ name, tenantId, meta: Object }

Google OAuth

  1. Create an OAuth 2.0 Client ID in Google Cloud Console. Under Authorized redirect URIs, add the exact URL of your callback (the value you'll put in GOOGLE_CALLBACK_URL). Under Authorized JavaScript origins, add the origin of your frontend (e.g. http://localhost:3000).
  2. Set env vars:
    GOOGLE_CLIENT_ID=<client id>
    GOOGLE_CLIENT_SECRET=<client secret>
    GOOGLE_CALLBACK_URL=http://localhost:4000/api/auth/google/callback
    OAUTH_SUCCESS_REDIRECT=http://localhost:3000/
    The callback URL points at the API server (this library handles it). The success redirect points at your frontend — that's where the user lands with ?accessToken=...&refreshToken=... appended for the SPA to read.
  3. Pass them into the engine:
    blueprintEngine(secret, {
      oauth: {
        google: {
          clientID:        process.env.GOOGLE_CLIENT_ID,
          clientSecret:    process.env.GOOGLE_CLIENT_SECRET,
          callbackURL:     process.env.GOOGLE_CALLBACK_URL,
          successRedirect: process.env.OAUTH_SUCCESS_REDIRECT,  // optional, defaults to '/'
        },
      },
    });
  4. The library mounts GET /google and GET /google/callback on the auth router. The callback issues access + refresh tokens and redirects to successRedirect?accessToken=...&refreshToken=....

Browser-initiated OAuth cannot carry an x-tenant-id header, so OAuth users land under tenantId: 'global'. The setTenant middleware automatically skips OAuth provider paths, so global mounting is safe.

Common gotcha — Error 400: redirect_uri_mismatch: the value you send to Google as redirect_uri must match a URI registered on the OAuth client exactly (scheme, host, port, path, no trailing slash differences). The string in your GOOGLE_CALLBACK_URL env var and the entry in Google Cloud Console must be character-for-character identical.

Facebook and Twitter strategies ship installed but are not yet wired into the auth router. Wiring arrives in v1.1.

MFA

Two-phase login when user.mfa.enabled === true:

  1. POST /login → returns { mfaPendingToken } (5-minute JWT, contains mfaPending: true).
  2. POST /mfa/challenge with { mfaPendingToken, code } → returns real access + refresh tokens.

Enrollment flow

  1. GET /mfa/setup (bearer token) — generates a TOTP secret, AES-encrypts it onto the user doc, returns { otpauthUrl, qrCodeDataURL }. MFA is not yet enabled.
  2. User scans the QR in their authenticator app.
  3. POST /mfa/verify-setup with the first 6-digit code — enables MFA, returns 8 raw backup codes (shown only once, stored bcrypt-hashed).

Email-method MFA (user.mfa.method === 'email') generates a 6-digit OTP stored on the user doc for 10 minutes and fires hooks.onMFAChallenge(user, 'email', otpCode) for delivery.

Backup codes work as one-time fallbacks in both /mfa/challenge and /mfa/disable.

Security notes

  • jwtSecret must be a long random string in production. 32+ bytes from crypto.randomBytes(32).toString('hex') is a good choice. The same secret signs both access and refresh tokens.
  • Access tokens expire in 15 minutes. Refresh tokens expire in 30 days, stored as SHA-256 hashes — the raw token is returned once to the client at login.
  • Passwords are bcrypt-hashed (10 rounds). There is no minimum length enforced at the library layer — enforce your policy with a validator before calling /register.
  • TOTP secrets are AES-encrypted with options.mfaSecret (falls back to jwtSecret) before DB storage.
  • Backup codes are bcrypt-hashed.
  • Login is rate-limited (10/15min per tenantId+IP) and brute-force protected (5 failures → 15-minute lockout per tenantId:email).
  • Token revocation: logout blacklists the access token. If REDIS_URL is set and ioredis is installed, the blacklist is shared across instances — otherwise it's in-memory per process.

License

MIT — see LICENSE.

Author

Swayam Pandya