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

triostack-suite-auth

v1.7.0

Published

Shared JWT signing/verification and Express auth middleware for Trio-Suite backend microservices

Downloads

46

Readme

triostack-suite-auth

Shared JWT signing/verification and Express auth middleware for Trio-Suite backend microservices. Every service shares one JWT_SECRET, so a token issued by one service verifies in every other -- this package exists so that logic isn't copy-pasted per service.

Install

npm install triostack-suite-auth

Usage

const { signToken, verifyToken, AUTH_TYPES, requireAuthType, requireSuperAdmin, requireAnyAuth, requireInternalToken } = require("triostack-suite-auth");

// Issue a token (JWT_SECRET read from process.env.JWT_SECRET by default)
const token = signToken({ userId, tenantId, organizationId, authType: AUTH_TYPES.USER }, { expiresIn: "8h" });

// Require a specific authType -- sets req.auth to the decoded payload
router.get("/me/software-access", requireAuthType(AUTH_TYPES.USER), controller.mySoftwareAccess);
router.get("/me/tenant-software-access", requireAuthType(AUTH_TYPES.TENANT), controller.tenantSoftwareAccess);

// Super admin also requires isSuperAdmin === true on the token
router.get("/admin/tenants", requireSuperAdmin(), controller.list);

// Any authenticated session, no authType restriction
router.get("/me", requireAnyAuth(), controller.me);

// Internal service-to-service token (constant-time compare), not a JWT --
// each service names its own env var, so pass a getter
router.use("/internal/permissions", requireInternalToken(() => process.env.IDENTITY_INTERNAL_API_TOKEN));

// Permission check -- must run AFTER a middleware that sets req.auth. A
// "tenant" session is granted every permission automatically (no lookup);
// a "user" session is checked for real via your resolver, since this
// package has no DB connection of its own -- see examples/requirePermission.example.js
// for both an in-process DB lookup and a cross-service internal-API lookup.
router.delete(
    "/leads/:id",
    requireAuthType(AUTH_TYPES.USER),
    requirePermission("crm.lead.delete", async (req) => {
        const { userId, organizationId } = req.auth;
        // ... your own DB query or internal API call, returns string[] of allowed codes
        return findAllowedPermissionCodesForUser(userId, organizationId, req.params.softwareId);
    }),
    controller.deleteLead
);

Cross-module (wildcard) permission codes

A permission code is "<scope>.<action...>", e.g. "crm.product.add". A role can be granted a wildcard-scoped code, "*.product.add", which satisfies any required code with the same suffix -- "crm.product.add", "inventory.product.add", "invoicing.product.add", etc. -- without granting one row per module. This is a real, storable string (permission codes are plain VARCHAR), not a query-time pattern -- *.product.add must actually exist as a permissions.code row and be linked via role_permissions, same as any other code.

requirePermission already matches wildcards automatically (it calls hasPermission internally). Use permissionCodeMatches/hasPermission directly wherever you check codes outside of requirePermission (e.g. inside an internal check-bulk endpoint):

const { permissionCodeMatches, hasPermission } = require("triostack-suite-auth");

permissionCodeMatches("*.product.add", "crm.product.add"); // true
permissionCodeMatches("crm.product.add", "inventory.product.add"); // false -- scoped grants never cross modules

hasPermission(["crm.lead.view", "*.product.add"], "invoicing.product.add"); // true

A bare "*" (no suffix) never matches anything, and "*" is only meaningful as the scope -- "crm.*" is not treated as "every action in crm."

Checking multiple permissions (requirePermissions)

For a route that needs more than one code -- either "must have all of these" or "must have at least one of these" -- use requirePermissions instead of stacking multiple requirePermission calls (which would call your resolver once per call). Same tenant-bypass/fail-closed rules as requirePermission, resolver called at most once:

const { requirePermissions } = require("triostack-suite-auth");

// AND (default): needs BOTH codes
router.post(
    "/leads/:id/assign",
    requireAuthType(AUTH_TYPES.USER),
    requirePermissions(["crm.lead.view", "crm.lead.assign"], resolveAllowedCodesForUser),
    controller.assignLead
);

// OR: needs AT LEAST ONE
router.get(
    "/leads/:id",
    requireAuthType(AUTH_TYPES.USER),
    requirePermissions(["crm.lead.view", "crm.lead.viewAll"], resolveAllowedCodesForUser, { mode: "any" }),
    controller.getLead
);

requirePermissions is array-based only -- if the required code(s) depend on the request itself (e.g. a route shared across CRM/Inventory/Invoicing where the module comes from req.params), wrap it in your own middleware that builds the array first, same as you would with requirePermission:

router.post("/:softwareCode/products", requireAnyAuth(), (req, res, next) =>
    requirePermissions([`${req.params.softwareCode}.product.add`], resolveAllowedCodesForUser)(req, res, next)
);

Subscription check (requireValidSubscription)

requirePermission/requirePermissions answer "what is this session allowed to do" -- they are NOT a subscription check, and a tenant session is never exempt from needing a real subscription (tenant sessions get every permission for free, never a free subscription -- those are separate rules). requireValidSubscription is the third gate: it runs for every authType that reaches it, no bypass for anyone, since "does this organization actually pay for this software" applies identically regardless of who's asking.

const { requireAnyAuth, requireValidSubscription, requirePermissions } = require("triostack-suite-auth");

// Your own check -- typically subscriptionValidation.service.js's
// validateSubscription (in-service) or a call to the internal
// tenant-access/validate endpoint (cross-service). organizationId is
// sourced differently per session: a "user" token carries it directly
// (req.auth.organizationId); a "tenant" token does NOT (see
// generateTenantToken) -- resolve it from wherever your app tracks it for
// a tenant session (query param, body, frontend-selected org state, ...).
const resolveSubscriptionValid = async (req) => {
    const { authType, tenantId } = req.auth;
    const organizationId = authType === "user" ? req.auth.organizationId : req.query.organizationId;
    const check = await subscriptionValidation.validateSubscription(tenantId, organizationId, req.params.softwareId);
    return check.allowed;
};

router.post(
    "/:softwareId/leads/:id/assign",
    requireAnyAuth(),                                          // 1. who are you
    requireValidSubscription(resolveSubscriptionValid),         // 2. does your org pay for this software (checked for EVERY authType)
    requirePermissions(["crm.lead.view", "crm.lead.assign"], resolveAllowedCodesForUser), // 3. what can you do
    controller.assignLead
);

If resolveIsValid returns false, the request is denied with 403. If it throws (subscription-service unreachable, DB down), the response is 503, not 403 -- an unreachable dependency is a different failure mode than a real, confirmed denial, and must not be reported as one.

Requiring ANY of several software subscriptions, by name (resolveSubscribedToAnyOf)

For a route that should pass if the organization is subscribed to any one of several software products -- named, not by numeric id -- resolveSubscribedToAnyOf builds a resolveIsValid for requireValidSubscription without you hand-rolling the OR loop:

const { requireAnyAuth, requireValidSubscription, resolveSubscribedToAnyOf, requirePermissions } = require("triostack-suite-auth");

// Your own lookups -- this package has no DB connection, so YOU supply
// how a software name resolves to an id, and how one id's subscription is
// actually checked.
const getSoftwareIdByName = async (name) => {
    const result = await pool.query(`SELECT id FROM software WHERE LOWER(name) = LOWER($1)`, [name]);
    return result.rows[0]?.id ?? null;
};

const isSubscriptionValid = async (req, softwareId) => {
    const { tenantId } = req.auth;
    const organizationId = defaultResolveOrganizationId(req); // or your own lookup
    const check = await subscriptionValidation.validateSubscription(tenantId, organizationId, softwareId);
    return check.allowed;
};

router.post(
    "/:softwareId/leads/:id/assign",
    requireAnyAuth(),
    requireValidSubscription(
        resolveSubscribedToAnyOf(["CRM", "Payroll"], { getSoftwareIdByName, isSubscriptionValid })
    ),
    requirePermissions(["crm.lead.view", "crm.lead.assign"], resolveAllowedCodesForUser),
    controller.assignLead
);
  • A name with no matching software (typo, renamed module) is treated as "not subscribed" for that name, not a crash -- the OR still checks the remaining names.
  • By default (failureMode: "allSettled"), one name's check throwing (e.g. that specific lookup is unreachable) does not sink the whole OR if another name's check confirms true. Pass { failureMode: "all" } to instead fail the whole check -- and therefore requireValidSubscription's 503 -- the instant any one check throws.
  • defaultResolveOrganizationId(req) (also exported) is the same "user token has organizationId directly, tenant token doesn't" fallback described above, exported standalone since isSubscriptionValid implementations need it too.

Design notes

  • signToken/verifyToken default to process.env.JWT_SECRET; pass { secret } to override.
  • requireAuthType/requireSuperAdmin/requireAnyAuth/requirePermission/requirePermissions/requireValidSubscription are Express middleware factories -- call them (requireAuthType(AUTH_TYPES.USER)), don't pass the function reference directly.
  • These are the same security boundary the per-service middleware they replace already was: real, not just a UX gate. Frontend route guards are a separate, non-authoritative layer (see @triostack/auth in triosuite-ui-service) -- don't confuse the two.
  • requirePermission/requirePermissions fail closed: if your resolver throws (DB down, internal API unreachable), the request is denied with 500, never silently allowed. requireValidSubscription fails closed too, but with 503 (service unavailable) instead of 500/403, since it's specifically about an unreachable dependency, not a confirmed denial.
  • requirePermissions never calls your resolver more than once per request, regardless of how many codes are being checked or which mode is used.
  • requirePermission/requirePermissions skip the resolver entirely for a "tenant" session (full permission access). requireValidSubscription does NOT skip tenant sessions -- subscription validity is checked for every authType, no exceptions. Put requireValidSubscription before requirePermission/requirePermissions on a route so an unsubscribed organization is rejected before permissions are even considered.