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

@xivdyetools/worker-kit

v1.4.1

Published

Shared Cloudflare Worker toolkit for xivdyetools: Hono middleware (request ID, logger, rate limit) + request body guards + magic-byte image sniffing + rate limiting backends (Memory, KV, Upstash)

Readme

@xivdyetools/worker-kit

Shared Cloudflare Worker toolkit for XIV Dye Tools: Hono middleware (request ID, structured logger, rate limiting), request-body guards, magic-byte image sniffing, plus the sliding-window rate limiting engine and backends (Cloudflare, Memory, KV, Upstash) it wraps.

npm version License: MIT

Formed in the Monorepo 2.0 Tier 1 consolidation by merging @xivdyetools/worker-middleware (v1.2.0) and @xivdyetools/rate-limiter (v1.5.0). Both APIs are unchanged — only the import specifiers moved. See DEPRECATIONS.md for the migration table.

Installation

pnpm add @xivdyetools/worker-kit

Optional peer dependencies: hono ^4.13.7 and @cloudflare/workers-types ^4.0.0 || ^5.0.0. Both are optional so a consumer that only needs the rate-limiter engine, not the middleware, never pulls in Hono — no current in-repo consumer is rate-limiter-only (see Consumers below).

Import Paths

// Middleware (root export, or ./middleware)
import {
  requestIdMiddleware, loggerMiddleware, rateLimitMiddleware,
  getRequestId, getLogger,
  type MiddlewareVariables,
} from '@xivdyetools/worker-kit';

// Request body guards (./body-guards only — it imports hono/body-limit at runtime)
import { bodyGuards } from '@xivdyetools/worker-kit/body-guards';

// Magic-byte image sniffing (root export, or ./image-sniff) — no Hono needed
import { detectImageFormat, sniffImageType } from '@xivdyetools/worker-kit/image-sniff';

// Rate limiter
import {
  MemoryRateLimiter, KVRateLimiter,
  getClientIp, getRateLimitHeaders, PUBLIC_API_LIMITS,
} from '@xivdyetools/worker-kit/rate-limiter';

// Single backend, for the leanest possible bundle
import { UpstashRateLimiter } from '@xivdyetools/worker-kit/rate-limiter/upstash';

The root export re-exports every module except ./body-guards (subpath-only, see below); the subpaths (./middleware, ./body-guards, ./image-sniff, ./rate-limiter, ./rate-limiter/{memory,kv,upstash,cloudflare,presets}) keep bundles lean.

Middleware

import {
  requestIdMiddleware,
  loggerMiddleware,
  rateLimitMiddleware,
  getRequestId,
  getLogger,
} from '@xivdyetools/worker-kit';
import type { MiddlewareVariables } from '@xivdyetools/worker-kit';

// Extend with your app's variables
type Variables = MiddlewareVariables & {
  auth: AuthContext;
};

const app = new Hono<{ Bindings: Env; Variables: Variables }>();

// Canonical order: requestId → logger → rateLimit (each reads the previous one's context)
app.use('*', requestIdMiddleware());

app.use('*', loggerMiddleware({
  serviceName: 'xivdyetools-presets-api',
  readApiVersionFromEnv: true,
}));

// In handlers:
app.get('/api/example', (c) => {
  const logger = c.get('logger');
  logger.info('Processing request');
  return c.json({ ok: true });
});

// In error handlers:
app.onError((err, c) => {
  const requestId = getRequestId(c);
  const logger = getLogger(c);
  logger?.error('Unhandled error', err);
  return c.json({ error: 'Internal error', requestId }, 500);
});

requestIdMiddleware(options?)

Generates or preserves an X-Request-ID header for distributed tracing.

| Option | Type | Default | Description | |--------|------|---------|-------------| | validateFormat | boolean | true | Validate an incoming X-Request-ID against UUID format. Malformed values are replaced via crypto.randomUUID() — a log-injection defense. |

loggerMiddleware(options)

Creates a per-request structured logger (via @xivdyetools/logger) and logs request start/completion with timing.

| Option | Type | Default | Description | |--------|------|---------|-------------| | serviceName | string | required | Service name for log aggregation. | | readEnvironmentFromEnv | boolean | true | Read ENVIRONMENT from c.env. When false, defaults to 'production'. | | readApiVersionFromEnv | boolean | false | Read API_VERSION from c.env. | | logUserAgent | boolean | false | Include User-Agent in the "Request started" log. | | sanitizePath | (path: string) => string | — | Redact sensitive URL segments before logging. |

rateLimitMiddleware(options)

Wires a RateLimiter backend into the request path, sets standard headers, and returns 429 when exhausted.

  • Backend memoization — the backend factory's result is cached per isolate. Never construct a MemoryRateLimiter inside the factory, or every request gets a fresh empty window.
  • Fail-open by default — backend errors let requests through (and log). Pass onError: 'fail-closed' to return 429 instead.
  • Standard headers — X-RateLimit-Limit / -Remaining / -Reset on every response; Retry-After on a 429.

Helpers and types

| Export | Description | |--------|-------------| | getRequestId(c) | Extract the request ID from Hono context. Returns 'unknown' if the middleware hasn't run. | | getLogger(c) | Extract the logger from Hono context. Returns undefined if the middleware hasn't run. | | MiddlewareVariables | { requestId: string; logger: ExtendedLogger } — extend with your app-specific variables. |

Body Guards (/body-guards)

Two middleware from one factory: a streaming request-body size cap (SEC-004) and a JSON depth / prototype-pollution check (SEC-003). The factory never writes a response body — every rejection is rendered by a caller-supplied responder, so two Workers with different error envelopes share one implementation without either changing a byte of what it returns.

import { bodyGuards } from '@xivdyetools/worker-kit/body-guards';

const { bodySizeLimit, jsonDepthLimit } = bodyGuards<{ Bindings: Env }>({
  maxSize: 10 * 1024,
  onTooLarge: (c) =>
    c.json({ error: 'Payload too large', message: 'Request body too large' }, 413),
  onInvalidJson: (c, message) =>
    c.json({ success: false, error: 'Invalid request body', message }, 400),
});

app.use('/auth/*', bodySizeLimit);
app.use('/auth/*', jsonDepthLimit);

| Option | Type | Default | Description | |--------|------|---------|-------------| | maxSize | number | required | Default body cap in bytes. | | maxDepth | number | 10 | Maximum JSON nesting depth. The budget is inclusive — a leaf at maxDepth is accepted, one at maxDepth + 1 is not. | | onTooLarge | (c) => Response | required | Response for an oversized body. | | onInvalidJson | (c, message) => Response | required | Response for unparseable JSON ('Invalid JSON syntax'), a depth violation (`JSON nesting exceeds maximum depth of ${maxDepth}`) or a pollution key ('Invalid JSON structure'). | | exempt | { match, maxSize, onTooLarge } | — | One request shape that gets a different cap and skips the JSON check entirely — e.g. a binary upload route. |

  • bodySizeLimit wraps Hono's bodyLimit, which decides on Content-Length alone when the header is present and otherwise counts the stream and cuts it at the cap — a header that understates the body is trusted, so a route that must not be lied to keeps its own post-read backstop.
  • jsonDepthLimit inspects POST / PATCH / PUT requests whose Content-Type includes application/json. A non-JSON content type, an empty body, and a body that cannot be read all pass through untouched. It rejects an own __proto__, constructor or prototype key at any level.
  • exempt.match is asked once per request by each middleware, so a route that is allowed a large binary body never pays for a JSON parse it cannot use.
// The one route allowed a large, non-JSON body — its own cap, its own error.
exempt: {
  match: (c) => c.req.method === 'POST' && UPLOAD_PATH.test(c.req.path),
  maxSize: 5 * 1024 * 1024,
  onTooLarge: (c) =>
    c.json({ success: false, error: 'VALIDATION_ERROR', message: 'Image must be at most 5 MB' }, 400),
}

Types: BodyGuardsOptions, BodyGuardMiddleware, BodyGuardExemption, BodyGuardResponder, InvalidJsonResponder.

Image Sniffing (/image-sniff)

Magic-byte format detection for PNG, JPEG, GIF, WebP and BMP. No Hono, no Workers types — a plain Uint8Array in, a format out, so an upload route can decide on content rather than on a Content-Type header a client controls.

import { detectImageFormat, sniffImageType } from '@xivdyetools/worker-kit/image-sniff';

detectImageFormat(bytes);                          // 'png' | … | undefined
sniffImageType(bytes, ['png', 'jpeg', 'webp']);    // 'png' | 'jpeg' | 'webp' | null

| Export | Description | |--------|-------------| | ImageFormat | 'png' \| 'jpeg' \| 'gif' \| 'webp' \| 'bmp' | | IMAGE_MAGIC_BYTES | The leading-byte table, keyed by format. webp's entry is the RIFF prefix only. | | detectImageFormat(bytes) | The format, or undefined. | | sniffImageType(bytes, accept?) | The format when it is on accept, otherwise null. The return type narrows to the accepted list. |

Both require at least 12 bytes and return nothing for a shorter buffer: the WebP check reads offsets 8–11, so a short buffer starting with RIFF would otherwise be decided on bytes that are not there. RIFF alone is never WebP — WAV and AVI share the container.

Rate Limiter (/rate-limiter)

A sliding-window rate limiting engine with four interchangeable backends.

import { KVRateLimiter, getClientIp, getRateLimitHeaders, PUBLIC_API_LIMITS }
  from '@xivdyetools/worker-kit/rate-limiter';

const limiter = new KVRateLimiter({ kv: env.RATE_LIMIT, keyPrefix: 'api:ip:' });
const result = await limiter.check(getClientIp(c.req.raw), PUBLIC_API_LIMITS);

if (!result.allowed) {
  return c.json({ error: 'Rate limited' }, 429, getRateLimitHeaders(result));
}

Backends

| Backend | Subpath | Use when | |---------|---------|----------| | CloudflareRateLimiter | /rate-limiter/cloudflare | Preferred per-client limiter (FINDING-003). Native [[ratelimits]] bindings, tiered; counts atomically per colo with no storage writes. | | MemoryRateLimiter | /rate-limiter/memory | Single isolate, tests, local dev. Not shared across isolates. | | KVRateLimiter | /rate-limiter/kv | Cloudflare KV. Eventually consistent — cannot throttle a fast client (1 write/s/key, swallowed put failures); a fallback only. | | UpstashRateLimiter | /rate-limiter/upstash | Upstash Redis. A real distributed sliding window; the strictest option. |

CloudflareRateLimiter trades exactness for atomicity and is documented as such: remaining reports limit - 1 while allowed and 0 when denied (the binding does not expose a count), checkOnly() consumes a slot while increment() is a no-op, reset() / resetAll() are no-ops, and counters are per-colo rather than global.

Utilities

| Export | Description | |--------|-------------| | getClientIp(request, options?) | Prefers CF-Connecting-IP. Never trusts X-Forwarded-For, which is spoofable (SEC-002). | | getRateLimitHeaders(result) | Builds the X-RateLimit-* / Retry-After header set. |

Presets (/rate-limiter/presets)

Shared limit configurations so every worker enforces the same policy:

OAUTH_LIMITS, getOAuthLimit(), DISCORD_COMMAND_LIMITS, getDiscordCommandLimit(), MODERATION_LIMITS, getModerationLimit(), PUBLIC_API_LIMITS.

Types

RateLimitResult, RateLimitConfig, RateLimiter, ExtendedRateLimiter, MemoryRateLimiterOptions, KVRateLimiterOptions, UpstashRateLimiterOptions, CloudflareRateLimiterOptions, CloudflareRateLimitTier, RateLimitBinding, RateLimiterLogger, GetClientIpOptions.

Middleware types (root / ./middleware): MiddlewareVariables, RequestIdOptions, LoggerMiddlewareOptions, RateLimitMiddlewareOptions.

Worker Configuration Examples

// discord-worker — no ENVIRONMENT env var, no user agent
app.use('*', requestIdMiddleware());
app.use('*', loggerMiddleware({
  serviceName: 'xivdyetools-discord-worker',
  readEnvironmentFromEnv: false,
}));

// presets-api — has ENVIRONMENT + API_VERSION
app.use('*', requestIdMiddleware());
app.use('*', loggerMiddleware({
  serviceName: 'xivdyetools-presets-api',
  readApiVersionFromEnv: true,
}));

// moderation-worker — custom URL sanitizer
import { sanitizeUrl } from './utils/url-sanitizer.js';
app.use('*', requestIdMiddleware());
app.use('*', loggerMiddleware({
  serviceName: 'xivdyetools-moderation-worker',
  readEnvironmentFromEnv: false,
  sanitizePath: sanitizeUrl,
}));

Dependencies

| Package | Purpose | |---------|---------| | @xivdyetools/logger | ExtendedLogger, createRequestLogger | | @upstash/redis | Upstash rate-limiter backend | | hono | Optional peer — needed only for the middleware module | | @cloudflare/workers-types | Optional peer — Workers type definitions |

Consumers

All seven backend apps: discord-worker, moderation-worker, presets-api, oauth, api-worker, og-worker and image-worker (middleware only). stoat-worker does not depend on this package — it was dropped along with svg/core (it renders no cards and needs no Workers-only middleware) — and the web app never has.

Connect With Me

Flash Galatine | Midgardsormr (Aether)

🎮 FFXIV: Lodestone Character 💻 GitHub: @FlashGalatine 🐦 X/Twitter: @AsheJunius 📺 Twitch: flashgalatine 🌐 BlueSky: projectgalatine.com ❤️ Patreon: ProjectGalatine ☕ Ko-Fi: flashgalatine 💬 Discord: Join Server

License

MIT © 2025-2026 Flash Galatine — see LICENSE.

Legal Notice

FINAL FANTASY is a registered trademark of Square Enix Holdings Co., Ltd. FINAL FANTASY XIV © SQUARE ENIX CO., LTD.

XIV Dye Tools is an unofficial fan project and is not affiliated with, endorsed by, or sponsored by Square Enix Co., Ltd.