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

@gobing-ai/ts-utils

v0.2.3

Published

Zero-dependency TypeScript utilities for dates, cursors, errors, output, origins, roles, and API responses.

Readme

@gobing-ai/ts-utils

Shared utilities — error types, date helpers, cursor-based pagination, role-based access control, API response builders, and output formatting. No platform dependencies; works in Bun, Node, and Cloudflare Workers.

Overview

ts-utils provides small, composable utilities used across ts-runtime, ts-db, ts-infra, and application code. Each module is self-contained with zero external dependencies.

| Module | Purpose | |--------|---------| | errors | Typed application errors (AppError, NotFoundError, ValidationError, etc.) | | date | nowMs(), timestamp ↔ Date conversion | | cursor | Base64url-encoded cursor-based pagination | | access | Zitadel + generic role-based access control (hasRole, getRoles) | | api-response | Standard API response envelope builders | | output | Human-readable output helpers (tables, JSON, etc.) | | origin | Request origin parsing | | const | Shared constants |

Architecture

classDiagram
    class AppError {
        +ErrorCode code
        +string message
    }

    class NotFoundError {
    }

    class ValidationError {
    }

    class ConflictError {
    }

    class InternalError {
        +unknown cause?
    }

    class ErrorCode {
        <<enumeration>>
        NotFound
        Validation
        Conflict
        Internal
    }

    class CursorData {
        <<interface>>
        +string id
        +number createdAt?
        +number offset?
    }

    class CursorHelpers {
        +createCursor(id, createdAt?, offset?) CursorData
        +parseCursor(data) CursorData
        +encodeCursor(cursor) string
        +decodeCursor(encoded) string
        +encodeCursorFromItem(id, createdAt?, offset?) string
        +decodeAndParseCursor(encoded) CursorData
        +buildCursorMeta(items, limit, hasMore) object
    }

    class DateHelpers {
        +nowMs() number
        +toMs(input) number | null
        +fromMs(ms) Date | null
    }

    class AccessControl {
        +hasRole(profile, role) boolean
        +getRoles(profile) string[]
    }

    AppError <|-- NotFoundError
    AppError <|-- ValidationError
    AppError <|-- ConflictError
    AppError <|-- InternalError
    AppError --> ErrorCode

How It Works

Error types

Typed error hierarchy with error codes — consumers catch by type, not by string:

throw new NotFoundError(`User ${userId} not found`);
throw new ValidationError('Email is required');
throw new InternalError('DB connection lost', originalError);

// Callers can discriminate:
if (error instanceof NotFoundError) {
    return new Response(null, { status: 404 });
}
if (isAppError(error)) {
    // error.code is typed: 'NOT_FOUND' | 'VALIDATION' | 'CONFLICT' | 'INTERNAL'
}

Cursor-based pagination

Encode a cursor from the last item in a page, return it to the client, decode on the next request:

import { encodeCursorFromItem, decodeAndParseCursor } from '@gobing-ai/ts-utils';

// Page 1: encode cursor from last item
const cursor = encodeCursorFromItem(lastItem.id, lastItem.createdAt);
// → "eyJpZCI6ImFiYyIsImNyZWF0ZWRBdCI6MTcwMDAwMDAwMH0"

// Page 2: decode cursor from query param
const parsed = decodeAndParseCursor(requestCursor);
// → { id: "abc", createdAt: 1700000000 }

// Build pagination metadata
const meta = buildCursorMeta(items, limit, hasMore);
// → { nextCursor: "eyJ...", hasMore: true, limit: 20 }

Cursor flow:

sequenceDiagram
    Client->>API: GET /items?limit=20
    API->>DB: SELECT ... LIMIT 20
    DB-->>API: 20 rows
    API->>API: encodeCursorFromItem(lastItem)
    API-->>Client: { items: [...], nextCursor: "eyJ..." }

    Client->>API: GET /items?limit=20&cursor=eyJ...
    API->>API: decodeAndParseCursor("eyJ...")
    API->>DB: SELECT ... WHERE id > "abc" LIMIT 20
    DB-->>API: 20 rows
    API-->>Client: { items: [...], nextCursor: "..." }

Role-based access control

Supports Zitadel IAM roles and generic role arrays/objects:

import { hasRole, getRoles } from '@gobing-ai/ts-utils';

// Zitadel profile
const profile = {
    'urn:zitadel:iam:org:project:roles': { admin: 'project-1', viewer: 'project-1' },
};
hasRole(profile, 'admin'); // → true
getRoles(profile); // → ['admin', 'viewer']

// Generic roles array
hasRole({ roles: ['editor', 'viewer'] }, 'editor'); // → true

// Object-based roles
hasRole({ roles: { admin: true } }, 'admin'); // → true

Date utilities

import { nowMs, toMs, fromMs } from '@gobing-ai/ts-utils';

const ts = nowMs(); // → 1700000000000

toMs(new Date('2024-01-01')); // → 1704067200000
toMs('2024-01-01'); // → 1704067200000
toMs(null); // → null

fromMs(1704067200000); // → Date('2024-01-01')
fromMs(null); // → null

Usage

Install

bun add @gobing-ai/ts-utils

Common patterns

import {
    AppError, NotFoundError, ValidationError, ConflictError, InternalError,
    nowMs, toMs, fromMs,
    encodeCursorFromItem, decodeAndParseCursor, buildCursorMeta,
    hasRole, getRoles,
} from '@gobing-ai/ts-utils';

// Errors
function getUser(id: string) {
    const user = db.find(id);
    if (!user) throw new NotFoundError(`User ${id} not found`);
    return user;
}

// Pagination
async function listUsers(limit: number, cursor?: string) {
    const parsed = cursor ? decodeAndParseCursor(cursor) : undefined;
    const users = await db.query(limit, parsed?.id);
    const hasMore = users.length === limit;
    return {
        items: users,
        ...buildCursorMeta(users, limit, hasMore),
    };
}

// Auth guard
function requireAdmin(profile: unknown) {
    if (!hasRole(profile as Record<string, unknown>, 'admin')) {
        throw new ForbiddenError('Admin access required');
    }
}