@freshpointcz/fresh-auth
v0.0.2
Published
HMAC service-to-service authentication, JWT verification, role definitions, and input validators for FreshPoint internal services
Keywords
Readme
@freshpointcz/fresh-auth
Authentication and authorization utilities for FreshPoint internal services. Provides HMAC-based service-to-service communication, JWT verification, role definitions, and input validators — all in one package.
Table of Contents
Installation
npm install @freshpointcz/fresh-authRequires Node.js >= 20.
Core
constructHeadersForInternalComms
Builds the three HTTP headers needed to authenticate an outbound service-to-service request using HMAC. The apiKey is never transmitted — only the MAC derived from it.
import { constructHeadersForInternalComms } from "@freshpointcz/fresh-auth";
const headers = constructHeadersForInternalComms("payment-service", process.env.API_KEY);
// {
// "x-service-name": "payment-service",
// "x-timestamp": "1716652800000",
// "x-service-key": "<sha512 HMAC hex digest>"
// }Signature
function constructHeadersForInternalComms(
serviceName: string,
apiKey: string,
options?: {
now?: number; // ms since epoch — inject in tests for deterministic output
algorithm?: string; // default: "sha512"
encoding?: "base64" | "base64url" | "hex" | "binary"; // default: "hex"
}
): CallerAuthHeadersWire contract — the receiving service must:
- Read
x-service-nameand look up the sharedapiKeyregistered for that service. - Recompute
HMAC(apiKey, "<serviceName>.<timestamp>")with the same algorithm and encoding. - Compare the result against
x-service-keyusing constant-time comparison (crypto.timingSafeEqual), never===. - Reject requests whose
x-timestampfalls outside the acceptable replay window.
Example — with axios
await axios.get(`${authBase}/verify/access-token`, {
headers: {
...constructHeadersForInternalComms(config.serviceName, apiKey),
Authorization: `Bearer ${token}`,
},
});Verifier
Verifies JWT access tokens and incoming HMAC service keys against the identity server (fresh-identity). Delegates HTTP to an injected RequestStrategy so the class has no hard dependency on any HTTP client.
Constructor
import { Verifier, AxiosStrategy } from "@freshpointcz/fresh-auth";
const verifier = new Verifier(
"payment-service", // this service's name
process.env.API_KEY, // this service's HMAC secret
new AxiosStrategy(axios),
{
authServerBaseUrl: "http://fresh-identity:8080", // default
tokenVerificationPath: "/verify/access-token", // default
apiKeyVerificationPath: "/verify/api-key", // default
}
);verifyJWT(token)
Sends the token to the identity server and returns the authenticated user. Attaches HMAC headers automatically so the auth server can trust the call.
const user: AuthenticatedUser = await verifier.verifyJWT(token);Throws:
UnauthorizedError— token missing, malformed, or expired (401)ForbiddenError— service not authorized to call this endpoint (403)AuthServiceError— any other non-2xx from the identity server
verifyApiKey(incomingHeaders)
Verifies that a set of x-service-* headers received from another service represent a valid HMAC signature. Forwards them as x-verify-* headers to the identity server alongside this service's own HMAC authentication.
const { serviceName } = await verifier.verifyApiKey({
"x-service-name": req.headers["x-service-name"],
"x-timestamp": req.headers["x-timestamp"],
"x-service-key": req.headers["x-service-key"],
});Throws:
UnauthorizedError— signature invalid or timestamp outside the acceptable window (401)ForbiddenError— service not authorized to call this endpoint (403)AuthServiceError— any other non-2xx from the identity server
Request Strategies
Abstract base and two built-in adapters for the HTTP client used by Verifier.
AxiosStrategy — wraps an axios instance, unwrapping the { data } envelope.
import axios from "axios";
import { AxiosStrategy } from "@freshpointcz/fresh-auth";
const strategy = new AxiosStrategy(axios);
// or with a custom instance:
const strategy = new AxiosStrategy(axiosInstance);FetchStrategy — wraps any WHATWG fetch-compatible function: native fetch (Node 18+), node-fetch v3+, or undici.
import { FetchStrategy } from "@freshpointcz/fresh-auth";
const strategy = new FetchStrategy(fetch);Custom strategy — extend RequestStrategy and implement a single method:
import { RequestStrategy } from "@freshpointcz/fresh-auth";
class MyStrategy extends RequestStrategy {
async get<T>(url: string, headers: Record<string, string>): Promise<T> {
// ...
}
}Errors
All errors extend AuthServiceError so you can catch the base class for a single handler or the subclasses for fine-grained control.
| Class | HTTP status | Default message |
|---|---|---|
| AuthServiceError | any (base) | — |
| UnauthorizedError | 401 | "Token is invalid or expired" |
| ForbiddenError | 403 | "Insufficient permissions" |
import { AuthServiceError, UnauthorizedError, ForbiddenError } from "@freshpointcz/fresh-auth";
try {
const user = await verifier.verifyJWT(token);
} catch (err) {
if (err instanceof UnauthorizedError) {
// 401 — invalid/expired token
} else if (err instanceof ForbiddenError) {
// 403 — valid token but not allowed
} else if (err instanceof AuthServiceError) {
// other identity server error — err.status has the HTTP code
}
}All three classes correctly restore the prototype chain so instanceof works even when compiled to ES5.
Defaults
import { DEFAULT_ALGORITHM, DEFAULT_ENCODING } from "@freshpointcz/fresh-auth";
DEFAULT_ALGORITHM // "sha512"
DEFAULT_ENCODING // "hex"Both constructHeadersForInternalComms and the identity server use these defaults. Override only when both sides agree on a different value.
Types
AuthenticatedUser
Shape returned by Verifier.verifyJWT and transformUserToAuthenticatedUser.
interface AuthenticatedUser {
id: number;
uuid: string;
email: string | null;
firstName: string | null;
middleName: string | null;
lastName: string | null;
phone: string | null;
emailValidated: boolean;
lastLoginAt: Date | null;
companyId: number;
roles: Role[];
}Headers
CallerAuthHeaders — headers produced by constructHeadersForInternalComms and consumed by Verifier.verifyApiKey.
interface CallerAuthHeaders {
"x-service-name": string;
"x-timestamp": string;
"x-service-key": string;
}AuthHeadersToVerify — re-keyed version sent to the identity server when verifying an incoming service's identity. Used internally by Verifier.verifyApiKey.
interface AuthHeadersToVerify {
"x-verify-service-name": string;
"x-verify-timestamp": string;
"x-verify-service-key": string;
}Roles
Every user in the identity system has one or more roles. Roles are grouped by department.
Role — union of all role string literals.
type Role = UserRole | AdminRoleType | DepotRole | CustomerRole | OperationRole | SaleRole;ALL_ROLES — flat readonly array of every valid role string, useful for runtime checks.
| Type | Constant | Values |
|---|---|---|
| AdminRoleType | ADMIN_ROLES | superadmin, admin, technician, developer |
| DepotRole | DEPOT_ROLES | driver, picker, order-receiver, driver-receiver, stock-controller, inventory-clerk, eshop-handler, depot-master |
| CustomerRole | CUSTOMER_ROLES | customer, ambassador |
| OperationRole | OPERATION_ROLES | operation-specialist, depot-order-master |
| SaleRole | SALES_ROLES | salesman, marketing-specialist, customer-care |
| UserRole (deprecated) | USER_ROLES | user |
Role descriptions
superadmin— full access; GOD figureadmin— standard admin accesstechnician— technology team, e.g. kiosk-ui accessdeveloper— technology team, e.g. kiosk-ui accessdriver— can restock devicespicker— can perform picking operationsorder-receiver— can accept new depot ordersdriver-receiver— handles products returned from driversstock-controller— can do write-offs and add old productsinventory-clerk— can perform inventory countseshop-handler— handles e-shop ordersdepot-master— total depot overlord; controls management unitscustomer— regular customerambassador— customer ambassadoroperation-specialist— operations departmentdepot-order-master— operations department, depot order authoritysalesman— sales departmentmarketing-specialist— marketing departmentcustomer-care— customer care specialist
DecoratorSecurityType
Enum-like string union used to annotate route handler security requirements.
type DecoratorSecurityType = "basic" | "token" | "basic-identifier" | "api-key";Utils
Hasher
Generates cryptographically random hex strings. Useful for generating API keys and secrets.
import { Hasher } from "@freshpointcz/fresh-auth";
Hasher.generate32HEX(); // 64-character hex string (32 random bytes)
Hasher.generate64HEX(); // 128-character hex string (64 random bytes)transformUserToAuthenticatedUser
Maps a raw database row or upstream API response to a typed AuthenticatedUser. Accepts both snake_case (direct database result) and camelCase (pre-converted upstream) input. Unknown or invalid role strings are silently dropped.
import { transformUserToAuthenticatedUser } from "@freshpointcz/fresh-auth";
// From a database row (snake_case)
const user = transformUserToAuthenticatedUser({
id: 1,
uuid: "abc-123",
email: "[email protected]",
first_name: "John",
middle_name: null,
last_name: "Doe",
phone: "+420731123456",
email_validated: true,
last_login_at: new Date(),
company_id: 42,
roles: [{ role: "admin" }, { role: "unknown-role" }], // "unknown-role" is dropped
});
// user.roles → ["admin"]
// From a camelCase source
const user2 = transformUserToAuthenticatedUser({
id: 1,
uuid: "abc-123",
// ... camelCase fields
roles: [{ role: "picker" }],
});EmailValidator
Validates email addresses against RFC 5321 structural constraints (max 254 chars, must contain @ and a domain with a dot). Does not check deliverability.
import { EmailValidator } from "@freshpointcz/fresh-auth";
// Nullable field (optional)
EmailValidator.isEmailInputValid("[email protected]"); // true
EmailValidator.isEmailInputValid(null); // true — absent value is valid
EmailValidator.isEmailInputValid(""); // true — absent value is valid
EmailValidator.isEmailInputValid("notanemail"); // false
EmailValidator.isEmailInputValid(42); // false
// Required field
EmailValidator.isRequiredEmailInputValid("[email protected]"); // true
EmailValidator.isRequiredEmailInputValid(null); // false — absent value is invalid
EmailValidator.isRequiredEmailInputValid(""); // falseInput is lowercased before matching, mirroring the application-layer Joi schema.
PhoneValidator
Validates phone numbers against E.164 format: +<1-3 digit country code><subscriber number>, max 16 characters total.
import { PhoneValidator } from "@freshpointcz/fresh-auth";
// Nullable field (optional)
PhoneValidator.isPhoneInputValid("+420731123456"); // true
PhoneValidator.isPhoneInputValid(null); // true — absent value is valid
PhoneValidator.isPhoneInputValid(""); // true — absent value is valid
PhoneValidator.isPhoneInputValid("0731123456"); // false — missing leading +
PhoneValidator.isPhoneInputValid("+0123456789"); // false — country code starts with 0
// Required field
PhoneValidator.isRequiredPhoneInputValid("+420731123456"); // true
PhoneValidator.isRequiredPhoneInputValid(null); // false
PhoneValidator.isRequiredPhoneInputValid(""); // falseIdentifierValidator
Validates and optionally normalises EAN-13 barcodes and RFID-10 identifiers. Short inputs are padded with leading zeros; inputs with recoverable leading zeros are trimmed. Inputs that are too long without leading zeros throw.
import { IdentifierValidator } from "@freshpointcz/fresh-auth";
// EAN-13 — validate only (coerce: false)
IdentifierValidator.isValidEAN("123", false); // true (padded: "0000000000123")
IdentifierValidator.isValidEAN("99999999999999", false); // false (14 digits, no leading zero)
IdentifierValidator.isValidEAN(null, false); // false
// EAN-13 — validate and return normalised string (coerce: true)
IdentifierValidator.isValidEAN("123", true); // "0000000000123"
IdentifierValidator.isValidEAN("00123456789012", true); // "0123456789012" (trimmed + padded)
// RFID-10 — same API, 10-character target length
IdentifierValidator.isValidRFID("123", false); // true
IdentifierValidator.isValidRFID("123", true); // "0000000123"
IdentifierValidator.isValidRFID("", false); // falseMINIMAL_CHAR_LENGTH = 1 — empty strings are always rejected.
isValidRole
Type guard that checks whether a string is a valid Role. Can be scoped to a specific role array for narrowed type inference.
import { isValidRole, DEPOT_ROLES } from "@freshpointcz/fresh-auth";
// Global check — narrows to Role
isValidRole("picker"); // true
isValidRole("foo"); // false
// Scoped check — narrows to DepotRole
isValidRole("picker", DEPOT_ROLES); // true
isValidRole("customer", DEPOT_ROLES); // false — customer is not a DepotRole
// Typical use: filtering raw role strings from the database
const roles = rawRoles.map(r => r.role).filter(isValidRole);
// roles: Role[]