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

@arcraz/common

v1.9.0

Published

Common utilities for the arcraz ecosystem

Readme

@arcraz/common

A TypeScript utilities library for the arcraz ecosystem, providing shared functionality across microservices.

Complete overhaul with factory pattern, Zod validation, and ESM-only build.

Requirements

  • Node.js 24+ (ESM-only)
  • Yarn package manager

Installation

yarn add @arcraz/common

Features

  • Factory Pattern - No singletons, full control over instances
  • Zod Config Validation - Type-safe configuration with runtime validation
  • Subpath Exports - Tree-shaking support for smaller bundles
  • Namespace Isolation - Redis keys and RabbitMQ queues are automatically namespaced by NODE_ENV
  • Native Drivers - Direct pg Pool and amqplib (no wrappers)
  • Error Hierarchy - Structured AppError base class with HTTP subclasses and type guard
  • Redis Rate Limiter - Fixed-window rate limiting with atomic Redis operations
  • Enhanced Repository - Soft-delete, sort-validated pagination, and findById out of the box

Quick Start

Configuration

import { loadEnv, getEnv, requireEnv } from '@arcraz/common/config';
import { DatabaseConfigSchema, RedisConfigSchema } from '@arcraz/common/config';

// Load .env file
loadEnv();

// Get environment variables
const nodeEnv = getEnv('NODE_ENV', 'development');
const dbHost = requireEnv('DATABASE_HOST'); // Throws if not set

// Validate configuration with Zod
const dbConfig = DatabaseConfigSchema.parse({
  name: 'main',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD
});

Database (PostgreSQL)

import { createDatabasePool } from '@arcraz/common/database';
import { BaseRepository, PagedResponseHelper, ConverterHelper } from '@arcraz/common/database';

// Create a connection pool
const pool = createDatabasePool({
  name: 'main',
  host: 'localhost',
  port: 5432,
  database: 'myapp',
  user: 'api',
  password: 'secret'
});

// Direct query
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);

// Using BaseRepository
class UserRepository extends BaseRepository {
  async getById(id: string) {
    return this.queryOne<User>('SELECT * FROM users WHERE id = $1', [id]);
  }

  async getPaged(page: number, pageSize: number) {
    const pager = new PagedResponseHelper(page, pageSize);
    const results = await this.query<User>(
      `SELECT *, COUNT(*) OVER() as full_count
       FROM users
       LIMIT $1 OFFSET $2`,
      [pager.limit, pager.offset]
    );
    return pager.result(results);
  }
}

const userRepo = new UserRepository(pool);
const user = await userRepo.getById('123');

// Cleanup
await pool.end();

Shallow camelize (preserve JSONB content)

By default, BaseRepository runs results through humps.camelizeKeys, which recurses into objects and arrays — every key inside a JSONB column is rewritten from snake_case to camelCase. That is wrong when JSONB content is part of an authoring contract (validated by Zod, written by migrations, referenced verbatim in mobile/UI code). Pass shallowCamelize: true to camelize only top-level row keys; values pass through by reference.

class GameRepository extends BaseRepository {
  constructor(db: DatabasePool, readOnlyDb?: DatabasePool) {
    // top-level row keys become camelCase, but theme_config JSONB is untouched
    super(db, readOnlyDb, { shallowCamelize: true });
  }

  async findById(id: string) {
    return this.queryOne<{ id: string; themeConfig: ThemeConfig }>('SELECT * FROM games WHERE id = $1', [id]);
  }
}

// game.themeConfig.branding.display_name stays 'display_name' (not 'displayName')

EnhancedRepository accepts the same option as a fourth constructor argument:

super(db, { table: 'games', primaryKey: 'id' }, readOnlyDb, { shallowCamelize: true });

If you need the helpers directly (without the repository class), the database subpath also exports camelCaseRowsShallow, camelCaseRowShallow, queryWithCamelCaseShallow, and queryOneWithCamelCaseShallow.

pg-promise pool (createPgPromisePool)

createDatabasePool above is pg-based. If your data layer is built on pg-promise (db.one/oneOrNone/none/manyOrNone, $/named/ params, :json, db.tx, pgp.helpers.ColumnSet), use createPgPromisePool instead. It returns both the db handle and the pgp root, and matches the Arcraz game-backend convention: automatic snake→camel column mapping on every query (via a receive hook), the shared type parsers (NUMERIC→float, BIGINT→number with an overflow guard, DATE→string), and a verified-TLS posture.

import { createPgPromisePool } from '@arcraz/common/database';

const { db, pgp, close } = createPgPromisePool({
  connectionString: process.env.DATABASE_URL!,
  maxConnections: 20,
  caCert: process.env.DATABASE_CA_CERT // PEM; verifies the managed-DB issuer
});

// Idiomatic pg-promise queries; columns arrive camelCased.
const user = await db.oneOrNone('SELECT * FROM users WHERE id = $/id/', { id });

// pgp.helpers for multi-row inserts (ColumnSet), transactions via db.tx(), etc.

// On shutdown / test teardown — end the pool (else connections leak / exit hangs):
process.on('SIGTERM', () => void close());

TLS posture: loopback connections (localhost / 127.0.0.1 / ::1, matched on the parsed hostname — not a substring of the URL) skip TLS for dev; every other connection verifies the upstream certificate — with caCert if provided, else the system trust store. rejectUnauthorized: false is never produced, and an unparseable connection string fails closed (TLS required).

Timeouts & pooling: a statementTimeoutMs (default 30s) aborts runaway queries so they can't exhaust the pool; maxConnections defaults to 20. All pools share one memoized pg-promise root (per pg-promise's singleton requirement) — pgp is that shared root.

Enhanced Repository

import { createDatabasePool, EnhancedRepository } from '@arcraz/common/database';
import type { EnhancedRepositoryConfig } from '@arcraz/common/database';

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

class UserRepository extends EnhancedRepository<User> {
  constructor(db: DatabasePool, readOnlyDb?: DatabasePool) {
    super(
      db,
      {
        table: 'users',
        primaryKey: 'id',
        softDelete: true,
        sort: {
          allowedColumns: ['name', 'email', 'created_at'],
          defaultColumn: 'created_at',
          defaultDirection: 'DESC'
        }
      },
      readOnlyDb
    );
  }
}

const pool = createDatabasePool({/* ... */});
const userRepo = new UserRepository(pool);

// Find by primary key (respects soft-delete)
const user = await userRepo.findById(123);

// Paginated + sorted (sort column validated against allowlist)
const page = await userRepo.findAll({
  page: 1,
  pageSize: 20,
  sortBy: 'name',
  sortDir: 'ASC'
});
// { pageNumber: 1, pageCount: 5, totalCount: 100, results: [...] }

// Soft-delete (sets is_active = false)
await userRepo.deleteById(123);

// Restore soft-deleted row
await userRepo.restoreById(123);

// Invalid sort columns throw (SQL injection prevention)
await userRepo.findAll({ sortBy: 'DROP TABLE users;--' }); // throws Error

Redis

createRedisClient() returns a RedisInstance wrapper ({ client, namespace, close(), isConnected() }) — not a raw ioredis client. Use the exported operation helpers (set, get, del, …), which take the instance as their first arg and namespace keys automatically. The raw ioredis client is available at redis.client if you need it.

import { createRedisClient, set, get, del } from '@arcraz/common/redis';

// Create client
const redis = createRedisClient({
  host: 'localhost',
  port: 6379
});

// Operation helpers namespace the key by NODE_ENV automatically.
// With NODE_ENV=production the stored key is "{production}:user:123".
await set(redis, 'user:123', JSON.stringify({ name: 'John' }), { ttl: 3600 });
const data = await get(redis, 'user:123');
await del(redis, 'user:123'); // del/exists/expire take a positional useNamespace (default true)

// Cleanup
await redis.close();

Also exported: createRedisCluster({ nodes: [...] }) for cluster deployments; list helpers (lpush/rpush/lrange/…); and the namespace helpers (getNamespacedKey, stripNamespace, …). set/get take an options object ({ ttl, nx, xx, useNamespace }); del/exists/expire take a positional useNamespace boolean.

Redis Rate Limiter

import { createRedisClient, createRateLimiter } from '@arcraz/common/redis';

const redis = createRedisClient({ host: 'localhost' });

// Create a rate limiter (fixed-window algorithm)
const apiLimiter = createRateLimiter(redis, {
  prefix: 'api',
  maxRequests: 100,
  windowSizeSeconds: 60,
  namespaced: true // prefix keys with {NODE_ENV}: — see note below (default: false)
});

// Check rate limit for a user/IP
const result = await apiLimiter.checkLimit('user-123');
// {
//   allowed: true,
//   current: 1,
//   limit: 100,
//   remaining: 99,
//   resetAtMs: 1735689660000
// }

if (!result.allowed) {
  throw new RateLimitError('Too many requests');
}

// Different limiters for different endpoints
const loginLimiter = createRateLimiter(redis, {
  prefix: 'login',
  maxRequests: 5,
  windowSizeSeconds: 300 // 5 minutes
});

// Reset a specific identifier's limit
await apiLimiter.resetLimit('user-123');

namespaced (default false) prefixes rate-limit keys with the {NODE_ENV}: namespace. When multiple environments/services share one Redis, set it to true so staging and prod don't share counters. It's left opt-in to avoid silently re-scoping existing limiters on a package upgrade — a one-time warning is logged if it's left false in a deployed (non-local) environment.

RabbitMQ

import { createRabbitMQConnection, publish, consume, getNamespacedQueue } from '@arcraz/common/rabbitmq';

// Create connection with reconnection options
const conn = await createRabbitMQConnection({
  host: 'localhost',
  port: 5672,
  username: 'guest',
  password: 'guest',
  appName: 'my-service',

  // Reconnection settings (all optional, shown with defaults)
  reconnectDelayMs: 5000, // Base delay between attempts
  reconnectMaxDelayMs: 60000, // Max delay cap (1 minute)
  reconnectBackoffMultiplier: 2, // Exponential backoff multiplier
  maxReconnectAttempts: 10 // Max attempts (0 = infinite)
});

// Queues are namespaced by NODE_ENV
// With NODE_ENV=production: "production-my-queue"
const queueName = getNamespacedQueue('my-queue');

await conn.channel.assertQueue(queueName, { durable: true });

// Publish messages
await publish(conn, 'my-queue', { event: 'user.created', data: { id: '123' } });
// Also available: publishToExchange(conn, exchange, data, routingKey?) for fanout/
// topic routing, and publishWithDelay(conn, queue, data, delayMs) (needs the
// rabbitmq-delayed-message-exchange broker plugin).

// Consume messages
// On a handler error (and when noAck is not set), the message is nacked WITHOUT
// requeue to avoid a poison-message loop. Without a dead-letter exchange it is
// then DROPPED — configure deadLetterExchange so failures are routed there for
// inspection/replay. The exchange must already exist.
await consume(
  conn,
  'my-queue',
  async (msg, content) => {
    console.log('Received:', content);
  },
  {
    deadLetterExchange: 'my-dlx', // optional; sets queue arg x-dead-letter-exchange
    deadLetterRoutingKey: 'my-queue.failed' // optional
  }
);

// Register onReconnect callback to re-setup after connection recovery
conn.onReconnect(async () => {
  await conn.channel.assertQueue(queueName, { durable: true });
  await consume(conn, 'my-queue', handler);
  console.log('Consumer re-registered after reconnect');
});

// Cleanup
await conn.close();

Reconnection Behavior

When the broker connection drops, createRabbitMQConnection automatically reconnects with exponential backoff and jitter:

| Attempt | Delay (base=5s, multiplier=2) | | ------- | ----------------------------- | | 1 | ~5s + jitter | | 2 | ~10s + jitter | | 3 | ~20s + jitter | | 4 | ~40s + jitter | | 5+ | ~60s + jitter (capped) |

  • Jitter (0–1s random) prevents thundering herd when multiple services reconnect simultaneously
  • maxReconnectAttempts: 0 disables the attempt limit for infinite retries
  • onReconnect(callback) runs after each successful reconnection — use this to re-register consumers and re-assert queues, since the old channel is replaced on reconnect

Caching

import { createRedisClient } from '@arcraz/common/redis';
import { ApiCache, DatabaseCache } from '@arcraz/common/caches';

const redis = createRedisClient({ host: 'localhost' });

// API Cache - for caching external API responses
const apiCache = new ApiCache(
  redis,
  'weather',
  async (city: string) => {
    const response = await fetch(`https://api.weather.com/${city}`);
    return response.json();
  },
  3600
); // 1 hour TTL

const weather = await apiCache.getCached('new-york');

// Database Cache - for caching query results
const dbCache = new DatabaseCache(
  redis,
  'users',
  async (id: string) => {
    return userRepo.getById(id);
  },
  300
); // 5 minute TTL

const user = await dbCache.getCached('123');
await dbCache.deleteCached('123'); // Invalidate on update

// Force a refresh (bypass cache, re-run the loader, re-cache)
const fresh = await dbCache.refreshCached('123');

// Pass parse=false to store/return the raw string instead of JSON-parsing
const raw = await dbCache.getCached('123', false); // string | null

Logging

import { createLogger } from '@arcraz/common/logging';

const logger = createLogger({
  level: 'info',
  format: 'json', // 'json' | 'logfmt' | 'pretty'
  serviceName: 'my-service'
  // Sensitive keys (password/token/apiKey/...) in the logged context are
  // redacted by default; set redact:false to opt out.
});

logger.info('Server started', { port: 3000 });
logger.error('Failed to connect', { error: err.message });

Security (Express Middleware)

import { createHelmetConfig, createOriginCorsConfig } from '@arcraz/common/security';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';

const app = express();

// Secure headers
app.use(helmet(createHelmetConfig()));

// CORS with an origin allowlist — createOriginCorsConfig is the intended helper
// for this; its first arg IS the origins list, and it enables credentials.
app.use(cors(createOriginCorsConfig(['https://myapp.com'], { credentials: true })));

// Or with the base factory (NOTE: the field is `origin`, singular — not `origins`;
// createCorsConfig spreads onto CorsConfig, so a mistyped `origins` is silently
// dropped, leaving CORS at the restrictive default):
// app.use(cors(createCorsConfig({ origin: ['https://myapp.com'], credentials: true })));

Also exported from @arcraz/common/security: createApiHelmetConfig (CSP-disabled variant for pure JSON APIs), createDevCorsConfig (permissive — dev only, never production), createOriginCorsConfig (allowlist, shown above), createDynamicCorsConfig (callback validator), plus the config types (HelmetConfig, CorsConfig, …).

Errors

import { AppError, BadRequestError, NotFoundError, ValidationError, InternalError, isAppError } from '@arcraz/common/errors';

// Throw typed HTTP errors with default status codes and messages
throw new NotFoundError('User not found');
throw new BadRequestError('Invalid email format');

// ValidationError supports field-level details
throw new ValidationError('Validation failed', {
  email: 'must be a valid email',
  password: 'must be at least 8 characters'
});

// InternalError defaults to isOperational=false (unexpected errors)
throw new InternalError('Database connection lost');

// Type guard for error handling middleware
app.use((err, req, res, next) => {
  if (isAppError(err)) {
    res.status(err.statusCode).json({
      code: err.code,
      message: err.message,
      details: err.details
    });
  } else {
    res.status(500).json({ code: 'INTERNAL_ERROR', message: 'Something went wrong' });
  }
});

Available error classes:

| Class | Status | Code | isOperational | | ------------------- | ------ | --------------------- | ------------- | | BadRequestError | 400 | BAD_REQUEST | true | | UnauthorizedError | 401 | UNAUTHORIZED | true | | ForbiddenError | 403 | FORBIDDEN | true | | NotFoundError | 404 | NOT_FOUND | true | | ConflictError | 409 | CONFLICT | true | | ValidationError | 422 | VALIDATION_ERROR | true | | RateLimitError | 429 | RATE_LIMIT_EXCEEDED | true | | InternalError | 500 | INTERNAL_ERROR | false |

Helpers

import {
  obscureData,
  removeSensitiveValues,
  redactSensitiveValues,
  DEFAULT_SENSITIVE_KEYS,
  convertToEnum,
  isValidEnumValue,
  getEnumValues,
  getEnumKeys
} from '@arcraz/common/helpers';

// Mask sensitive data (default masking char is '#'; pass placeholder to change it)
const masked = obscureData('4111111111111111', { showLeft: 4, showRight: 4 });
// "4111########1111"

// Remove sensitive fields. Key matching is CASE-INSENSITIVE and circular-safe.
// Pass DEFAULT_SENSITIVE_KEYS to scrub the common secret keys instead of a hand list.
const clean = removeSensitiveValues(
  { name: 'John', Password: 'secret', ssn: '123-45-6789' },
  ['password', 'ssn'] // matches 'Password' too (case-insensitive)
);
// { name: 'John' }

// Redact instead of remove
const redacted = redactSensitiveValues({ name: 'John', password: 'secret' }, DEFAULT_SENSITIVE_KEYS);
// { name: 'John', password: '[REDACTED]' }

// Enum conversion + inspection
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE'
}
const status = convertToEnum('ACTIVE', Status); // Status.Active
const isValid = isValidEnumValue('ACTIVE', Status); // true
const values = getEnumValues(Status); // ['ACTIVE', 'INACTIVE']
const keys = getEnumKeys(Status); // ['Active', 'Inactive']

The logging logger scrubs these same keys from log context by default — see Logging.

Socket.IO

Single-Namespace Server

import { createSocketIOServer } from '@arcraz/common/socketio';

const io = await createSocketIOServer({
  httpServer,
  cors: { origin: 'https://myapp.com', credentials: true },
  authVerify: async (auth) => {
    const user = await verifyToken(auth.token as string);
    return user; // attached to socket.data.user
  }
});

io.namespace.on('connection', (socket) => {
  io.trackConnection(socket.data.user.id, socket.id);
  socket.on('disconnect', () => io.untrackConnection(socket.id));
});

// Emit helpers
io.emitToUser('user-123', 'notification', { text: 'Hello' });
io.emitToRoom('lobby', 'message', { from: 'system', text: 'Welcome' });
io.broadcast('announcement', { text: 'Server restarting' });

Multi-Namespace Hub (Typed Events)

import { createSocketIOHub } from '@arcraz/common/socketio';
import type { EventMap } from '@arcraz/common/socketio';

// Define typed events per namespace
type ChatClientEvents = { sendMessage: (text: string) => void };
type ChatServerEvents = { newMessage: (msg: { from: string; text: string }) => void };

type NotifServerEvents = { alert: (data: { level: string; message: string }) => void };

const hub = await createSocketIOHub({
  httpServer,
  cors: { origin: 'https://myapp.com', credentials: true }
});

// Each namespace gets independent auth, events, tracking, and room management
const chat = hub.addNamespace<ChatClientEvents, ChatServerEvents, EventMap, User>({
  path: '/chat',
  authVerify: async (auth) => verifyToken(auth.token as string),
  eventHandlers: {
    sendMessage: (socket, text) => {
      chat.emitToRoom('general', 'newMessage', { from: socket.data.user.name, text });
    }
  },
  onConnection: (socket, roomManager) => {
    chat.trackConnection(socket.data.user.id, socket.id);
    roomManager.joinRoom(socket.id, 'channel', 'general');
  },
  onDisconnect: (socket) => {
    chat.untrackConnection(socket.id);
  }
});

const notifications = hub.addNamespace<EventMap, NotifServerEvents>({
  path: '/notifications'
});

// Typed emit — TypeScript enforces correct event names and argument types
chat.broadcast('newMessage', { from: 'system', text: 'Welcome!' });
notifications.emitToRoom('admins', 'alert', { level: 'warn', message: 'CPU high' });

// Room manager: structured naming
chat.roomManager.buildRoomName('workspace', 'w1', 'channel', 'general');
// -> 'workspace:w1:channel:general'

AWS S3

S3 helpers ship from @arcraz/common/aws/s3 — a functional wrapper over @aws-sdk/client-s3 (the client is passed explicitly, no singletons). Works against AWS S3 or any S3-compatible service (MinIO) via endpoint + forcePathStyle.

import {
  createS3Client,
  closeS3Client,
  upload,
  download,
  downloadAsBuffer,
  downloadAsString,
  exists,
  getMetadata,
  listObjects,
  copyObject,
  deleteObject,
  deleteObjects,
  getPresignedDownloadUrl,
  getPresignedUploadUrl
} from '@arcraz/common/aws/s3';

const s3 = createS3Client({
  region: 'us-east-1'
  // credentials optional — falls back to the default AWS credential chain.
  // endpoint + forcePathStyle for MinIO / S3-compatible services.
});

// Upload (body: Buffer | Readable | string)
await upload(s3, 'my-bucket', 'reports/2026.json', JSON.stringify(data), {
  contentType: 'application/json',
  cacheControl: 'max-age=3600',
  serverSideEncryption: 'AES256'
});

// Download — stream, buffer, or string (all return null when the key is absent)
const stream = await download(s3, 'my-bucket', 'reports/2026.json'); // Readable | null
const buf = await downloadAsBuffer(s3, 'my-bucket', 'reports/2026.json'); // Buffer | null
const text = await downloadAsString(s3, 'my-bucket', 'reports/2026.json'); // string | null

// Existence + metadata
await exists(s3, 'my-bucket', 'reports/2026.json'); // boolean
await getMetadata(s3, 'my-bucket', 'reports/2026.json'); // { key, size?, lastModified?, etag?, storageClass? } | null

// List (paginated via continuationToken)
const { objects, continuationToken } = await listObjects(s3, 'my-bucket', {
  prefix: 'reports/',
  maxKeys: 100
});

// Copy (source key is URL-encoded internally so spaces / '#' work)
await copyObject(s3, 'src-bucket', 'a/b.json', 'dst-bucket', 'a/b.json');

// Delete one or many (deleteObjects no-ops on an empty array)
await deleteObject(s3, 'my-bucket', 'reports/2026.json');
await deleteObjects(s3, 'my-bucket', ['a.json', 'b.json']);

// Presigned URLs (expiresIn defaults to 3600s)
const getUrl = await getPresignedDownloadUrl(s3, 'my-bucket', 'a.json', { expiresIn: 900 });
const putUrl = await getPresignedUploadUrl(s3, 'my-bucket', 'a.json', {
  expiresIn: 900,
  contentType: 'application/json'
});

// Cleanup
closeS3Client(s3);

AWS Bedrock

Bedrock runtime helpers ship from @arcraz/common/aws/bedrock — a thin wrapper over @aws-sdk/client-bedrock-runtime for invoking Claude models and generating Titan embeddings.

import { createBedrockClient, closeBedrockClient, invokeClaude, complete, generateEmbedding, generateEmbeddings } from '@arcraz/common/aws/bedrock';

const bedrock = createBedrockClient({ region: 'us-east-1' });

// Full message-based invocation
const res = await invokeClaude(bedrock, [{ role: 'user', content: 'Summarize this changelog...' }], {
  modelId: 'anthropic.claude-3-5-sonnet-20240620-v1:0', // default
  systemPrompt: 'You are a concise release-notes writer.',
  maxTokens: 4096,
  temperature: 0.7
});
// { content: string, stopReason: string, usage: { inputTokens, outputTokens } }

// One-shot text completion (returns just the string)
const text = await complete(bedrock, 'Write a haiku about caching.');

// Embeddings (Titan by default)
const one = await generateEmbedding(bedrock, 'hello world'); // { embedding: number[], inputTokens? }
const many = await generateEmbeddings(bedrock, ['a', 'b', 'c']); // EmbeddingResponse[]

closeBedrockClient(bedrock);

AWS CloudFront

CloudFront helpers ship from @arcraz/common/aws/cloudfront — signed URLs and cookies for protected origins plus invalidation for cache busting on publish flows.

import { createCloudFrontSigner, signUrl, createCloudFrontInvalidator, createInvalidation, closeCloudFrontInvalidator } from '@arcraz/common/aws/cloudfront';

// Signed URL for a protected origin
const signer = createCloudFrontSigner({
  keyPairId: process.env.CF_KEY_PAIR_ID!,
  privateKey: process.env.CF_PRIVATE_KEY!,
  distributionDomain: 'd123abc.cloudfront.net'
});
const url = signUrl(signer, '/levels/v7.json', {
  expires: new Date(Date.now() + 3600 * 1000)
});

// Invalidate a path after a content publish
const invalidator = createCloudFrontInvalidator({ region: 'us-east-1' });
const { id, status } = await createInvalidation(
  invalidator,
  process.env.CF_DISTRIBUTION_ID!,
  ['/levels/manifest.json'],
  { callerReference: 'content-v7' } // optional, defaults to a timestamp
);
console.log(`Invalidation ${id} → ${status}`);
closeCloudFrontInvalidator(invalidator);

createInvalidation prepends a leading / to any path that lacks one and rejects empty path lists. Reusing a callerReference is idempotent — CloudFront returns the existing invalidation instead of creating a new one — so a content-version id makes a good caller reference.

To protect a whole path prefix rather than a single object, use signed cookies instead of a signed URL:

import { signCookies, getCookieHeaders } from '@arcraz/common/aws/cloudfront';

const cookies = signCookies(signer, {
  resourcePath: '/levels/*', // defaults to '/*'
  expires: new Date(Date.now() + 3600 * 1000) // Date or Unix-seconds number
});
// { 'CloudFront-Policy', 'CloudFront-Signature', 'CloudFront-Key-Pair-Id' }

// Render as Set-Cookie values (Secure / HttpOnly / SameSite=Strict by default)
res.setHeader('Set-Cookie', getCookieHeaders(cookies, 'cdn.myapp.com'));

Types

import { Result, ok, err, isOk, isErr, unwrap, unwrapOr } from '@arcraz/common/types';

// Result type for error handling without exceptions
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return err('Division by zero');
  return ok(a / b);
}

const result = divide(10, 2);

if (isOk(result)) {
  console.log('Result:', unwrap(result)); // 5
}

// Or with default value
const value = unwrapOr(divide(10, 0), 0); // 0

Subpath Exports

Import only what you need for optimal tree-shaking:

import { ... } from '@arcraz/common';           // Everything
import { ... } from '@arcraz/common/config';    // Config & Zod schemas
import { ... } from '@arcraz/common/database';  // pg + pg-promise pools, BaseRepository, EnhancedRepository
import { ... } from '@arcraz/common/redis';     // Redis client, operations, rate limiter
import { ... } from '@arcraz/common/rabbitmq';  // RabbitMQ
import { ... } from '@arcraz/common/caches';    // Caching utilities
import { ... } from '@arcraz/common/logging';   // Logger
import { ... } from '@arcraz/common/security';  // Helmet & CORS configs
import { ... } from '@arcraz/common/errors';    // AppError hierarchy & isAppError guard
import { ... } from '@arcraz/common/helpers';   // Utility functions
import { ... } from '@arcraz/common/types';     // TypeScript types & enums
import { ... } from '@arcraz/common/constants'; // Default values
import { ... } from '@arcraz/common/socketio';  // Socket.IO server, hub, room manager
import { ... } from '@arcraz/common/aws/s3';        // S3 client, upload, presigned URLs
import { ... } from '@arcraz/common/aws/cloudfront'; // Signed URLs, signed cookies, invalidations
import { ... } from '@arcraz/common/aws/bedrock';    // Bedrock runtime client

Namespace Isolation

Redis and RabbitMQ resources are automatically namespaced by NODE_ENV to prevent collisions in shared environments:

| Service | Pattern | Example (NODE_ENV=staging) | | -------- | ---------------- | -------------------------- | | Redis | {NODE_ENV}:key | {staging}:user:123 | | RabbitMQ | NODE_ENV-queue | staging-notifications |

// Redis - curly braces ensure cluster slot compatibility
getNamespacedKey('user:123'); // "{staging}:user:123"
stripNamespace('{staging}:user:123'); // "user:123"
extractNamespace('{staging}:user:123'); // "staging"

// RabbitMQ
getNamespacedQueue('notifications'); // "staging-notifications"
getNamespacedExchange('events'); // "staging-events"
stripQueueNamespace('staging-notifications'); // "notifications"

Development

Scripts

yarn install    # Install dependencies
yarn lint       # Run ESLint
yarn test       # Run Vitest tests
yarn build      # Build to dist/

Testing

Tests use Vitest and are located in __tests__/:

yarn test              # Run all tests
yarn test:watch        # Watch mode
yarn test:coverage     # With coverage report

License

Apache-2.0 - See LICENSE for details.