npm package: @knsdev/node-utils
Description: TypeScript utilities for Node.js: JWT, bcrypt, Express cookies/helpers, OAuth, Turnstile, validation, Zod schemas, pagination, themes, and more (ESM).
Author: Khandaker Sajjat
Latest version: 3.4.1

# @knsdev/node-utils

TypeScript utilities for Node.js APIs: JWT (HS256) via [`JwtUtils`](#jwtutils), bcrypt passwords, Express cookies and small Express helpers, OAuth (Google / Facebook), Cloudflare Turnstile, HMAC, validation helpers, Zod schemas, pagination/sleep/theme helpers ([`getValidTheme`](#utils)), shared [`types`](#types) (including UI themes), and math/random utilities. Published as **ES modules** (`"type": "module"`).

## Requirements

- **Node.js** with native `fetch` (or a compatible runtime).
- **Express** is a peer-style dependency for [`CookieUtils`](#cookieutils-express) typings and `res.cookie`; install `express` in your app if you use cookies.

## Installation

```bash
pnpm add @knsdev/node-utils
# or
npm install @knsdev/node-utils
```

Build from source (for contributors). `build` runs `clean` first so `dist/` never keeps stale files after renames.

```bash
pnpm install
pnpm run build
# optional: pnpm run clean
```

## How to import

The package exposes **per-file subpaths** (no root barrel in `exports`):

```ts
import { JwtUtils } from "@knsdev/node-utils/JwtUtils.js";
import { tryCatch } from "@knsdev/node-utils/tryCatch.js";
```

Use the `.js` extension in import specifiers (NodeNext / TypeScript convention for ESM). Paths map to compiled files under `dist/` (e.g. `JwtUtils.ts` → `JwtUtils.js`).

For local monorepos, you can also re-export from the included [`src/index.ts`](src/index.ts) if your bundler resolves it.

---

## Modules and usage

### `tryCatch` / `tryCatch2`

Async and sync helpers that return a **tuple** `[data, error]` instead of throwing.

```ts
import { tryCatch, tryCatch2 } from "@knsdev/node-utils/tryCatch.js";

const [user, err] = await tryCatch(fetch("https://api.example.com/user"));
if (err) {
  console.error(err);
} else {
  console.log(await user.json());
}

const [parsed, parseErr] = tryCatch2(() => JSON.parse('{"a":1}'));
```

### `getEnvv`

Read `process.env` with a small schema: `string`, `int`, `float`, `boolean`, or `appEnv` (`dev` | `test` | `prod`). On failure, throws one `Error` whose message has up to two sections separated by a line of `================================`: `Not found keys: …` and/or `Value errors: …`.

```ts
import { getEnvv } from "@knsdev/node-utils/getEnvv.js";

const env = getEnvv({
  PORT: "int",
  DATABASE_URL: "string",
  APP_ENV: "appEnv",
  DEBUG: "boolean",
});

// env.PORT is number, env.APP_ENV is 'dev' | 'test' | 'prod', etc.
```

### `JwtUtils`

HS256 sign/verify via `jsonwebtoken`. Throwing variants use your API error types (`InternalServerError`, `Unauthorized`, `BadRequest`).

```ts
import { JwtUtils } from "@knsdev/node-utils/JwtUtils.js";

type Session = { sub: string; role: string };

const token = JwtUtils.signOrThrow<Session>(
  process.env.JWT_SECRET!,
  { sub: "user-1", role: "user" },
  15 * 60 * 1000, // 15 minutes (ignored if payload already has `exp`)
);

const payload = JwtUtils.verifyOrThrow<Session>(process.env.JWT_SECRET!, token);

const signResult = JwtUtils.sign<Session>(secret, payload, 3600_000);
if (signResult.error) {
  /* handle */
}
```

### `PasswordUtils`

Bcrypt hash/compare and simple length checks.

```ts
import { PasswordUtils } from "@knsdev/node-utils/PassUtils.js";

const hashResult = await PasswordUtils.hashPassword(
  "correct horse battery staple",
);
if (hashResult.success) {
  console.log(hashResult.hashedPassword);
}

const cmp = await PasswordUtils.comparePassword("plain", storedHash);
if (cmp.success) {
  /* logged in */
} else {
  /* cmp.error — e.g. BadRequest for wrong password */
}

try {
  await PasswordUtils.comparePasswordOrThrow("plain", storedHash); // true on match; throws on mismatch / bcrypt error
} catch (e) {
  /* handle */
}

if (!PasswordUtils.validateStrength(pw, 8, 128)) {
  /* reject */
}

if (PasswordUtils.isBcryptHash(value)) {
  /* already a hash */
}
```

### `CookieUtils` (Express)

HTTP-only cookies with secure defaults: `secure` follows environment (`dev` allows insecure unless `sameSite === 'none'`).

```ts
import type { Response } from "express";
import { CookieUtils } from "@knsdev/node-utils/Cookies.js";

function setSessionCookie(res: Response, token: string) {
  CookieUtils.setCookie(
    res,
    "session",
    token,
    7 * 24 * 60 * 60 * 1000,
    ".example.com",
    "prod",
    "/",
    "lax",
  );
}

function logout(res: Response) {
  CookieUtils.clearCookie(res, "session", ".example.com", "prod");
}
```

### `express` (helpers)

Typed `res.json` wrapper and async route wrapper that forwards errors to `next`.

```ts
import type { RequestHandler } from "express";
import { CatchExpress, SendRes } from "@knsdev/node-utils/express.js";

type Health = { ok: true };

const health: RequestHandler = (req, res) => {
  SendRes<Health>(res, { ok: true });
};

app.get("/health", CatchExpress(health));
```

### `utils`

`sleep(ms)` and `PageLimit(query)` for list endpoints (page, limit, skip, sort, optional `searchTerm`, remaining keys as `filtersData`). `getValidTheme(unknown)` coerces untrusted theme input to a valid `TThemes` (`'light' | 'dark' | 'system'`), defaulting to `'light'` when the value is missing or invalid (see also `AThemes` in [`types`](#types)).

```ts
import {
  getValidTheme,
  PageLimit,
  sleep,
} from "@knsdev/node-utils/utils.js";

await sleep(500);

const { page, limit, skip, sortBy, sortOrder, searchTerm, filtersData } =
  PageLimit(req.query);

const theme = getValidTheme(req.query.theme);
```

### `HMACUtils`

SHA-256 HMAC (hex) and timing-safe comparison.

```ts
import { HMACUtils } from "@knsdev/node-utils/HMACUtils.js";

const { data: digest, error } = HMACUtils.hash(
  process.env.WEBHOOK_SECRET!,
  rawBody,
);
const valid = digest !== null && HMACUtils.compare(digest, signatureFromHeader);
```

### `IDUtils` / `validateUUID`

Crypto random UUID (v4) and validation; `validateOrThrow` raises `BadRequest` with `'ID__INVALID'`.

```ts
import { IDUtils } from "@knsdev/node-utils/IDUtils.js";
import { validateUUID } from "@knsdev/node-utils/validators.js";

const id = IDUtils.gen();

if (IDUtils.validate(req.params.id)) {
  /* narrowed to string */
}

IDUtils.validateOrThrow(req.params.id); // throws BadRequest if invalid

if (validateUUID(someInput)) {
  /* someInput is string */
}
```

### `Rand`

Random integers/floats (crypto vs `Math.random`), UUIDs, and Base58 strings.

```ts
import {
  randInt,
  randUuid,
  randStr,
  srandInt,
} from "@knsdev/node-utils/Rand.js";

const code = randInt(100000, 999999);
const apiKey = randStr(32);
const id = randUuid();
const quickDemo = srandInt(1, 6); // non-crypto, for low-stakes UI
```

### `Nums`

Clamping, safe parsing, formatting, percentages.

```ts
import {
  clamp,
  parseIntSafe,
  formatWithCommas,
  percentage,
  toNumber,
} from "@knsdev/node-utils/Nums.js";

const n = clamp(Number(input), 0, 100);
const page = parseIntSafe(query.page, 10) ?? 1;
const label = formatWithCommas(12345.67, "en-US", 2);
const pct = percentage(3, 10, 1); // 30.0
const x = toNumber(unknownValue, 0);
```

### `HttpCodes` / `ErrorClasses` / `ECApiCodes`

HTTP status constants and errors for APIs: `ApiError` plus `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `TooManyRequests`, `InternalServerError`. Error `message` values use the `ECApiCodes` string union—machine-readable codes for clients (including auth/JWT/refresh, captcha, password and field validation, slug/username/profile fields, and simple success-style API codes such as `FETCHED` or `CREATED`).

```ts
import { HttpCodes } from "@knsdev/node-utils/HttpCodes.js";
import {
  BadRequest,
  Unauthorized,
  NotFound,
} from "@knsdev/node-utils/ErrorClasses.js";
import type { ECApiCodes } from "@knsdev/node-utils/ECApiCodes.js";

function handler() {
  throw new BadRequest("EMAIL__INVALID");
}

function requireUser(token: string | undefined) {
  if (!token) throw new Unauthorized("UNAUTHORIZED");
}

// Example Express error middleware pattern:
// if (err instanceof ApiError) res.status(err.status).json({ code: err.message });
```

### `GoogleUtils`

Verify Google **ID tokens** with `jose`, or exchange an **authorization code** for tokens and then resolve the user.

```ts
import { GoogleUtils } from "@knsdev/node-utils/GoogleUtils.js";

const fromIdToken = await GoogleUtils.getUserFromJwt(
  idToken,
  process.env.GOOGLE_CLIENT_ID!,
);

const fromCode = await GoogleUtils.getUserFromCode(
  code,
  process.env.GOOGLE_CLIENT_ID!,
  process.env.GOOGLE_CLIENT_SECRET!,
  "https://your.app/oauth/callback",
);
// Returns { oauth_id, name, email, picture } or null on failure
```

### `FacebookUtils`

Validates a **Facebook user access token** (not a JWT) via Graph API `debug_token`, then loads `/me`. Requests use POST bodies so tokens and app secrets stay out of query strings. Returns `{ oauth_id, name, email, picture }` or `null`. The Graph API version used by this module is defined in code as a single constant (currently `v19.0`) for easy upgrades.

```ts
import { FacebookUtils } from "@knsdev/node-utils/FBUtils.js";

const profile = await FacebookUtils.getUserFromAccessToken(
  accessToken,
  process.env.FB_APP_ID!,
  process.env.FB_APP_SECRET!,
);
```

### `TurnstileValidator`

Server-side [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) verification. Tuple API or throwing `BadRequest` with `'CAPTCHA__VALIDATION_FAILED'`.

```ts
import { TurnstileValidator } from "@knsdev/node-utils/TurnstileValidator.js";

const [success, failure] = await TurnstileValidator.validate(
  process.env.TURNSTILE_SECRET!,
  tokenFromClient,
  req.ip,
  { expectedAction: "login", expectedHostname: "your.app" },
  10_000,
  "prod",
);

if (failure) {
  console.error(failure["error-codes"]);
} else {
  console.log(success.hostname, success.challenge_ts);
}

// Or throw on any failure (last arg is reserved for API stability / future use; pass your app env):
const result = await TurnstileValidator.validateOrThrow(
  secret,
  tokenFromClient,
  req.ip,
  {},
  10_000,
  "prod",
);
```

### `bnUtils`

Bengali digit substitution and English month → Bengali month names.

```ts
import { bnNum, monthToBn } from "@knsdev/node-utils/bnUtils.js";

bnNum(2026); // Bengali digits for "2026"
monthToBn("january");
monthToBn(3);
```

### `types`

Shared string unions (app envs, roles, user status/gender, visibility, months, themes, etc.) plus `AThemes` (the runtime array of valid theme literals) and the `NoInfer<T>` helper type for generics.

```ts
import type { TAppEnvs, TThemes, TUserRoles } from "@knsdev/node-utils/types.js";
import { AThemes } from "@knsdev/node-utils/types.js";

function configure(env: TAppEnvs) {
  /* ... */
}

const isTheme = (v: string): v is TThemes => AThemes.includes(v as TThemes);
```

### `zod` (optional)

Prebuilt Zod schemas aligned with `CLengths` and machine-friendly error codes (`TZodCodes` / `ECApiCodes` style): `z_PASSWORD`, `z_GENERAL_TOKEN`, `z_CODE_6_DIGITS`, `z_SLUG`, `z_EMAIL`, `z_FIRST_NAME`, `z_LAST_NAME`, `z_TURNSTILE_TOKEN`, `z_LOGOUT_OTHER_SESSIONS`, `z_REMEMBER_ME`.

```ts
import { z_EMAIL, z_PASSWORD } from "@knsdev/node-utils/zod.js";
import { z } from "zod";

const LoginBody = z.object({
  email: z_EMAIL,
  password: z_PASSWORD,
});
```

---

## Dependencies (summary)

| Area                      | Packages                       |
| ------------------------- | ------------------------------ |
| JWT                       | `jsonwebtoken`                 |
| Google ID token           | `jose`                         |
| Passwords                 | `bcrypt`                       |
| Schemas                   | `zod`                          |
| Cookies + Express helpers | `express` (types / `Response`) |

---

## License

MIT — see [LICENSE](LICENSE).

## Links

- [Repository](https://github.com/KnsDev330/node-utils)
- [Issues](https://github.com/KnsDev330/node-utils/issues)