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

@celsian/rpc

v0.6.3

Published

Type-safe RPC utilities for CelsianJS

Downloads

257

Readme

@celsian/rpc

Type-safe RPC procedures with middleware, schema validation, and OpenAPI generation for CelsianJS.

Install

npm install @celsian/rpc

Usage

// server.ts
import { createApp } from '@celsian/core';
import { procedure, router, RPCHandler } from '@celsian/rpc';
import { z } from 'zod';

const app = createApp();

const appRouter = router({
  greet: procedure
    // `input` is inferred from the schema: no type argument, no cast.
    .input(z.object({ name: z.string() }))
    .query(({ input }) => `Hello, ${input.name}!`),
});
const rpc = new RPCHandler(appRouter);
rpc.mount(app); // serves /_rpc/* (pass a prefix to mount elsewhere: rpc.mount(app, '/api/rpc'))

export type AppRouter = typeof appRouter;
// client.ts
import { createRPCClient } from '@celsian/rpc/client';
import type { AppRouter } from './server.js';

const client = createRPCClient<AppRouter>({ baseUrl: 'http://localhost:3000/_rpc' });

// `input` is checked against the schema, and `greeting` is `string`.
const greeting = await client.greet.query({ name: 'Ada' });

mount() registers both GET and POST wildcard routes, the RPC client uses GET for queries and POST for mutations. (Note: CelsianApp has no .all() method.) If you prefer to register the routes yourself:

app.get('/_rpc/*path', (req) => rpc.handle(req));
app.post('/_rpc/*path', (req) => rpc.handle(req));

Security defaults

The handler ships with three defaults you should know about, because each one can reject a request that used to succeed.

1. Non-GET requests must be application/json

multipart/form-data, application/x-www-form-urlencoded, and text/plain are CORS-simple content types: a cross-origin <form method="post"> reaches them with the victim's cookies attached and no preflight. The mutation → POST rule is not a defense, because the attacker's form also uses POST. Requiring JSON forces a preflight, which a plain HTML form cannot satisfy.

Anything else gets 415 UNSUPPORTED_MEDIA_TYPE. Procedures that genuinely take file uploads opt in per procedure:

const appRouter = router({
  uploadAvatar: procedure.allowFormData().mutation(async ({ input }) => {
    const file = (input as FormData).get('file');
    // ...
  }),
});

An opted-in procedure is only as safe as the origin check below (or an app-level CSRF token), so opt in narrowly.

2. Cross-origin state-changing requests are rejected

Applied to every mutation and every non-GET request (a query is invokable over POST, so the verb carries no security meaning on its own):

| Request | Result | | --- | --- | | Origin matches the request URL's origin | allowed | | Origin listed in allowedOrigins | allowed | | Origin present and unrecognized | 403 CROSS_ORIGIN_DENIED | | No Origin, Sec-Fetch-Site: cross-site or same-site | 403 CROSS_ORIGIN_DENIED | | Neither header (curl, server-to-server, native client) | allowed |

same-site is rejected because a sibling subdomain shares cookies with you.

new RPCHandler(appRouter, {
  allowedOrigins: ['https://app.example.com'], // separate SPA host, or behind a proxy
  // originCheck: false,                        // opt out entirely (not recommended)
});

Set allowedOrigins when you sit behind a reverse proxy: the check compares against request.url's origin, which may not be your public origin.

3. Introspection is off in production

/_rpc/openapi.json and /_rpc/manifest.json list every procedure path (including admin.* and internal.*) with full input/output JSON Schemas. They are served before procedure lookup, so per-procedure middlewares never applied to them. They now default to development-only and 404 otherwise, indistinguishable from an unknown procedure.

new RPCHandler(appRouter, {
  introspection: true, // "development" (default) | true | false
  introspectionMiddlewares: [
    async ({ ctx, next }) => {
      if (!isAdmin(ctx.request)) throw new HttpError(403, 'Forbidden');
      return next();
    },
  ],
});

Logging

Pass the app logger so 5xx detail goes through its redaction, levels, and sinks instead of raw console.error, which a client can amplify into log flooding:

new RPCHandler(appRouter, { logger: app.log });

Wire protocol notes

decode() never copies __proto__, constructor, or prototype out of a payload, on any path, including GET ?input= and standalone handle() calls, neither of which passes through @celsian/core's body-parser scrub. It also caps nesting at 32 levels; deeper payloads become a clean 400 PARSE_ERROR.

RegExp values are deliberately decoded as strings, never reconstructed into a RegExp, so untrusted wire data cannot deliver a ReDoS pattern.

Documentation

See the main repository for full docs, examples, and API reference.

License

MIT