nhb-toolbox
Version:
A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.
450 lines (449 loc) • 17.1 kB
TypeScript
import type { GenericObject } from '../object/types';
import type { Maybe } from '../types/index';
import type { DecodedToken, SignetPayload, SignOptions, TokenString, VerifiedToken, VerifyOptions } from './types';
/**
* @class A lightweight, secure implementation of JWT-like tokens using `HMAC-SHA256` signatures.
* - This class provides methods to create, verify, and decode tokens with a simple API similar to JSON Web Tokens (`JWT`)
* but with a smaller footprint and zero dependencies.
*
* @remarks
* - **Features:**
* - `HMAC-SHA256` signatures for security
* - Time-based claims (expiration, not-before)
* - Standard claims (audience, issuer, subject)
* - Constant-time signature comparison to prevent timing attacks
* - Automatic date conversion for timestamp claims
* - `Base64` URL-safe encoding (standard `Base64` in this implementation)
*
* - **Security considerations:**
* - Keep the secret key secure and rotate periodically
* - Use appropriate token expiration times
* - Validate all claims relevant to your application
* - Store tokens securely (HTTP-only cookies recommended for web)
*
* @example
* ```typescript
* // Create a token signer
* const signet = new Signet('my-secret-key');
*
* // Sign a token with custom payload and options
* const token = signet.sign(
* { userId: 123, role: 'admin' },
* {
* expiresIn: '1h',
* audience: 'my-app',
* issuer: 'auth-service'
* }
* );
*
* // Verify a token
* const result = signet.verify(token, {
* audience: 'my-app',
* issuer: 'auth-service'
* });
*
* if (result.isValid) {
* console.log('Valid token for user:', result.payload.userId);
* } else {
* console.log('Invalid token:', result.error);
* }
*
* // Decode without verification
* const decoded = signet.decode(token);
* console.log('Token payload:', decoded.payload);
* ```
*/
export declare class Signet {
#private;
/**
* * Creates a new `Signet` instance with the specified secret key.
*
* @param secret - The secret key used for signing and verifying tokens.
* Must be a non-empty string.
*
* @throws If the secret is not a non-empty string.
*
* @remarks
* - The secret is converted to `UTF-8` bytes and stored internally.
* - Choose a strong secret (at least 32 characters) and store it securely.
* - For production, consider using key rotation strategies.
*
* @example
* ```typescript
* // Initialize with a secret key
* const signet = new Signet('super-secret-key-123');
*
* // Use environment variable for the secret
* const signet = new Signet(process.env.JWT_SECRET!);
* ```
*/
constructor(secret: string);
/**
* * Creates and signs a new token with the given payload and options.
*
* @param payload - Custom data to include in the token payload.
* Must be a `non-empty object`.
* @param options - Optional configuration for token claims and expiration.
*
* @returns A signed token string in the format `header.payload.signature`.
*
* @throws If payload is not a valid object.
*
* @remarks
* - **The token structure follows JWT format:**
* - Header: Contains algorithm (`HS256`) and token type (`SIGNET+JWT`)
* - Payload: Includes standard claims (`iat`, `exp`, `nbf`, `aud`, `sub`, `iss`) plus custom data
* - Signature: `HMAC-SHA256(signingInput, secret)` (the result of the hash)
* - Signing Inputs: `base64(header) + "." + base64(payload)` (the string that gets hashed)
*
* - **Automatic claims added:**
* - `iat` (issued at): Current time in seconds
* - `iatDate`: Current time as Date object
* - If `expiresIn` is provided: `exp` and `expDate`
* - If `notBefore` is provided: `nbf` and `nbfDate`
*
* @example
* ```typescript
* // Basic token with custom data
* const token = signet.sign({ userId: 123, name: 'John' });
*
* // Token with expiration and claims
* const token = signet.sign(
* { userId: 123 },
* {
* expiresIn: '2h',
* audience: 'api.example.com',
* issuer: 'auth-service',
* subject: 'user-123'
* }
* );
*
* // Token valid after 5 minutes
* const token = signet.sign(
* { action: 'reset-password' },
* { notBefore: '5m' }
* );
* ```
*/
sign(payload: GenericObject, options?: SignOptions): TokenString;
/**
* * Decodes a token without verifying its signature.
*
* @typeParam T - Type of custom data in the token payload.
* @param token - The token string to decode.
*
* @returns The decoded token parts including header, payload, and signatures.
*
* @throws If the token is malformed, empty, or cannot be parsed.
*
* @remarks
* - Use this method when you need to inspect token contents without verification.
* - **Warning:** This does not validate the signature, so the data may have been tampered with.
* - Always use {@link verify} method for security-critical operations.
* - The payload includes both timestamp values (numbers) and {@link Date} objects for convenience.
*
* @example
* ```typescript
* // Decode token to inspect contents
* const decoded = signet.decode(token);
* console.log('Header:', decoded.header);
* console.log('Payload:', decoded.payload);
* console.log('Signature:', decoded.signature);
*
* // Access custom payload data with type safety
* const decoded = signet.decode<{ userId: number }>(token);
* const userId = decoded.payload.userId; // Type: number
* ```
*/
decode<T extends GenericObject = GenericObject>(token: string): DecodedToken<T>;
/**
* * Checks if a token has expired based on its `exp` claim.
*
* @param token - The token to check.
*
* @returns `true` if the token has an `exp` claim and current time is past it,
* `false` if token has no expiration or is still valid.
*
* @throws If the token is malformed or cannot be decoded.
*
* @remarks
* - Tokens without `exp` claim are considered non-expiring (returns `false`)
* - Uses current system time for comparison ({@link Date.now()})
* - Does not verify the signature (use only with trusted tokens or after verification)
*
* @example
* ```typescript
* // Check expiration
* if (signet.hasExpired(token)) {
* console.log('Token has expired');
* // Prompt user to re-authenticate
* }
*
* // Use with other validation
* const isValid = !signet.hasExpired(token) && !signet.isTooEarly(token);
* ```
*/
hasExpired(token: string): boolean;
/**
* * Checks if a token's `nbf` (not-before) claim indicates it's too early to use.
*
* @param token - The token to check.
*
* @returns `true` if the token has an `nbf` claim and current time is before it,
* `false` if token has no `nbf` claim or is already valid.
*
* @throws If the token is malformed or cannot be decoded.
*
* @remarks
* - Useful for implementing time-based access control, like activation links that shouldn't be used until a certain time.
* - Uses current system time for comparison ({@link Date.now()})
* - Does not verify the signature (use only with trusted tokens or after verification)
*
* @example
* ```typescript
* // Check if token is active yet
* if (signet.isTooEarly(token)) {
* console.log('Token not valid yet');
* // Wait before using
* }
* ```
*/
isTooEarly(token: string): boolean;
/**
* * Validates a token's `iss` (issuer) claim against an expected value.
*
* @param token - The token to check.
* @param expected - The expected issuer value. If `undefined`, always returns `false`.
*
* @returns `true` if the token has an `iss` claim that doesn't match the expected value,
* `false` if issuer matches, token has no issuer claim, or expected issuer is undefined.
*
* @throws If the token is malformed or cannot be decoded.
*
* @remarks Use this to ensure tokens come from trusted sources in multi-issuer scenarios.
*
* @example
* ```typescript
* // Validate issuer
* if (signet.isInvalidIssuer(token, 'auth-service')) {
* console.log('Token from unexpected issuer');
* // Reject token
* }
*
* // With optional issuer check
* const issuer = process.env.EXPECTED_ISSUER;
* if (issuer && signet.isInvalidIssuer(token, issuer)) {
* throw new Error('Invalid issuer');
* }
* ```
*/
isInvalidIssuer(token: string, expected: Maybe<string>): boolean;
/**
* * Validates a token's `aud` (audience) claim against expected values.
*
* @param token - The token to check.
* @param expected - The expected audience(s). Can be a string or array of strings.
* If `undefined`, always returns `false`.
*
* @returns `true` if the token has an `aud` claim and none of its values match any of the expected audiences, `false` otherwise.
*
* @throws If the token is malformed or cannot be decoded.
*
* @remarks
* - Tokens can have single audience (string) or multiple audiences (string[])
* - Returns `false` (valid) if at least one audience matches
* - Useful for multi-tenant or multi-service architectures
*
* @example
* ```typescript
* // Single audience check
* if (signet.isInvalidAudience(token, 'api.example.com')) {
* console.log('Token not intended for this audience');
* }
*
* // Multiple allowed audiences
* const validAudiences = ['web-app', 'mobile-app', 'admin-panel'];
* if (signet.isInvalidAudience(token, validAudiences)) {
* throw new Error('Invalid audience');
* }
*
* // Token with multiple audiences
* // Token payload: { aud: ['web-app', 'mobile-app'] }
* // Check if at least one matches
* const isValid = !signet.isInvalidAudience(token, ['web-app', 'admin-panel']);
* // Returns false (valid) because 'web-app' matches
* ```
*/
isInvalidAudience(token: string, expected: Maybe<string | string[]>): boolean;
/**
* * Validates a token's `sub` (subject) claim against an expected value.
*
* @param token - The token to check.
* @param expected - The expected subject value. If `undefined`, always returns `false`.
*
* @returns `true` if the token has a `sub` claim that doesn't match the expected value,
* `false` if subject matches, token has no subject claim, or expected subject is undefined.
*
* @throws If the token is malformed or cannot be decoded.
*
* @remarks
* - Use this to ensure tokens are being used by the intended user/entity.
* - Common for authorization checks where tokens should be user-specific.
*
* @example
* ```typescript
* // Validate subject
* const userId = 'user-123';
* if (signet.isInvalidSubject(token, userId)) {
* console.log('Token not for this user');
* // Reject request
* }
*
* // Optional subject validation
* const expectedSubject = getExpectedSubjectFromRequest();
* if (expectedSubject && signet.isInvalidSubject(token, expectedSubject)) {
* return response.status(403).send('Invalid subject');
* }
* ```
*/
isInvalidSubject(token: string, expected: Maybe<string>): boolean;
/**
* * Verifies a token's signature and validates all claims.
*
* @typeParam T - Type of custom data in the token payload.
* @param token - The token string to verify.
* @param options - Optional validation criteria for token claims.
*
* @returns A {@link VerifiedToken} object indicating success or failure.
* - If valid: `{ isValid: true, payload: SignetPayload<T> }`
* - If invalid: `{ isValid: false, error: string }`
*
* @remarks
* - **Performs the following checks in order:**
* - Token structure (3 parts separated by dots)
* - Base64 decoding of header and payload
* - JSON parsing of header and payload
* - Signature verification (constant-time comparison)
* - Expiration check (if `exp` claim exists)
* - Not-before check (if `nbf` claim exists)
* - Issuer validation (if provided in options)
* - Audience validation (if provided in options)
* - Subject validation (if provided in options)
*
* - **This is the recommended method for most token validation scenarios.**
*
* @example
* ```typescript
* // Basic verification
* const result = signet.verify(token);
* if (result.isValid) {
* console.log('Valid token:', result.payload);
* } else {
* console.log('Invalid token:', result.error);
* }
*
* // With claim validation
* const result = signet.verify(token, {
* audience: 'api.example.com',
* issuer: 'auth-service',
* subject: 'user-123'
* });
*
* // Type-safe custom payload
* interface UserToken {
* userId: number;
* role: string;
* }
* const result = signet.verify<UserToken>(token);
* if (result.isValid) {
* const { userId, role } = result.payload;
* // userId and role are typed
* }
* ```
*/
verify<T extends GenericObject = GenericObject>(token: string, options?: VerifyOptions): VerifiedToken<T>;
/**
* * Verifies a token and throws an error if invalid.
*
* @typeParam T - Type of custom data in the token payload.
* @param token - The token string to verify.
* @param options - Optional validation criteria for token claims.
*
* @returns A valid {@link VerifiedToken} with `isValid: true`.
*
* @throws If the token is invalid, with a message describing the failure.
*
* @remarks
* - This method is a convenience wrapper around {@link verify} that throws instead of returning an error object.
* - Useful for `express`-style middleware or when you want to handle authentication failures with exceptions.
* - The thrown error message is the same as the `error` property in the invalid result from {@link verify}.
*
* @example
* ```typescript
* // Use in middleware/guard
* function authMiddleware(req, res, next) {
* const token = req.headers.authorization?.replace('Bearer ', '');
*
* try {
* const result = signet.verifyOrThrow(token, {
* audience: 'api.example.com'
* });
* req.user = result.payload;
* next();
* } catch (error) {
* res.status(401).json({ error: error.message });
* }
* }
*
* // In application code
* try {
* const result = signet.verifyOrThrow(token);
* // Token is guaranteed valid here
* processUserRequest(result.payload);
* } catch (error) {
* handleAuthError(error);
* }
* ```
*/
verifyOrThrow<T extends GenericObject = GenericObject>(token: string, options?: VerifyOptions): VerifiedToken<T>;
/**
* * Extracts only the payload from a token without full verification.
*
* @typeParam T - Type of custom data in the token payload.
* @param token - The token string to decode.
*
* @returns The token payload including standard claims and custom data.
*
* @throws If the token is malformed, empty, or cannot be parsed.
*
* @remarks
* - This is a convenience method equivalent to `decode(token).payload`.
*
* - **Security Note:** This method does NOT verify the token signature.
*
* - **Only use it when:**
* - You've already verified the token elsewhere
* - The token comes from a trusted source
* - You're debugging or logging
* - The operation is not security-critical
*
* - For security-sensitive operations, always use {@link verify} or {@link verifyOrThrow} method.
*
* @example
* ```typescript
* // Quick payload extraction for non-critical operations
* const payload = signet.decodePayload(token);
* console.log('User ID:', payload.userId);
* console.log('Issued at:', payload.iatDate);
*
* // Type-safe with custom interface
* interface AppToken {
* userId: number;
* permissions: string[];
* }
* const payload = signet.decodePayload<AppToken>(token);
* const canDelete = payload.permissions.includes('delete');
* ```
*/
decodePayload<T extends GenericObject = GenericObject>(token: string): SignetPayload<T>;
}