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

@fitfak/grpc

v2.0.0

Published

Zero-dependency gRPC server, client, reflection and health checking for Node.js (part of the FITFAK stack)

Readme

@fitfak/grpc

A gRPC stack for Node.js with zero npm dependencies — every module uses only node:http2, node:tls, node:zlib, node:crypto and node:events.

protobuf.js      proto3 wire codec (varint/zigzag/packed/nested/repeated)
wire.js          gRPC framing, compression negotiation, grpc-timeout, grpc-web trailers
status.js        status codes, GrpcError, grpc-message percent encoding
peer.js          TLS peer identity, security levels, RFC 9266 channel binding
grpc-server.js   HTTP/2 server: unary, server/client/bidi streaming, deadlines, mTLS gating
client.js        HTTP/2 client: all four call kinds, retries, keepalive, bootstrap→mTLS upgrade
application.js   GrpcApplication + registerController: schema generation, auth, RBAC
reflection.js    grpc.reflection.v1 and v1alpha (grpcurl, Postman)
health.js        grpc.health.v1.Health (Check + Watch)
descriptor.js    FileDescriptorProto construction for reflection
logger.js        component-tagged structured logger with hex dump
smtp-service.js  minimal SMTP-over-TLS sender

npm test runs four suites (107 checks) covering framing, all call kinds, mTLS and the application layer.


1. Quick start

const { GrpcApplication } = require('@fitfak/grpc');

const app = new GrpcApplication({ packageName: 'custom.network' });

app.setAuthenticator(async (call) => verifyJwt(call.metadata.authorization));

app.registerController('VaultService', {
  GetRecord: {
    requiresAuth: true,
    requiredPermissions: PERMISSIONS.READ_DATA,
    request:  [{ no: 1, name: 'id', type: 'string' }],
    response: [{ no: 1, name: 'payloadJson', type: 'string' }],
    handler: async (req, call) => ({ payloadJson: await load(req.id, call.user) }),
  },
});

app.listen(8443, { tls: { key, cert, ca, requestCert: true, rejectUnauthorized: true } });

Message types are generated as VaultService_GetRecordReq / VaultService_GetRecordRes, which is the naming the hand-written schema maps in existing client code already use — so a controller written this way is wire-compatible with them without a .proto file anywhere.

Client:

const { GrpcClient } = require('@fitfak/grpc');

const client = new GrpcClient('https://vault.internal:8443', {
  credentials: { key: clientKeyPem, cert: clientCertPem, ca: caPem },
  getMetadata: async () => ({ authorization: `Bearer ${await tokens.current()}` }),
});

const res = await client.unary('/custom.network.VaultService/GetRecord',
  schemas, 'VaultService_GetRecordReq', 'VaultService_GetRecordRes', { id: '42' });

2. Security levels, and why they exist

Every connection is classified from its TLS state, and every method can declare the minimum it will accept:

| Level | Meaning | |---|---| | none | plaintext h2c — no confidentiality, no peer identity | | tls | the server proved its identity; the client did not | | mtls | both sides presented certificates and the peer's chain validated |

app.registerController('EnrollmentService', {
  Enroll: { minSecurityLevel: 'tls',  /* ... */ },   // reachable before the client has a cert
});
app.registerController('DatabaseService', {
  Insert: { minSecurityLevel: 'mtls', /* ... */ },   // data plane, mTLS only
});

The check runs before the handler and before any middleware that might authenticate the caller some other way, so "this method is bootstrap-reachable" is a property of the method definition rather than something each handler has to remember.

To serve both channels on one port, configure the listener with requestCert: true and rejectUnauthorized: false: every client is asked for a certificate, a client without one still completes the handshake, and the per-stream classification does the rest. With rejectUnauthorized: true the refusal happens during the handshake instead — earlier and cheaper, but then no bootstrap endpoint can live on that port.

Verified vs. presented certificates

On a listener that accepts unauthenticated clients, a peer can present a certificate signed by any CA — including its own — and Node returns it from getPeerCertificate() exactly as it returns a valid one. call.peer.certificate is therefore populated only when the chain validated, so reading call.peer.certificate.commonName can never hand you an attacker-chosen string. The unverified form is available as call.peer.presentedCertificate for logging, under a name that cannot be mistaken for a trusted one.

Channel binding

call.peer.channelBinding is the RFC 9266 tls-exporter value for the connection, and client.channelBinding() is the same value on the other side. A bootstrap request signed over it cannot be replayed onto a different TLS connection, which is what makes an enrolment exchange safe on a channel where the client is not yet authenticated. It is null when the negotiated TLS version cannot export keying material — an exchange that depends on it must say so rather than silently proceeding unbound.

Upgrading a live client

TLS client certificates are chosen during the handshake, so a certificate cannot be added to an existing connection. client.upgrade(credentials) establishes a new session with the new credentials, waits until it is usable, and only then discards the old one — a failed upgrade leaves the client on its previous working channel rather than disconnected.

const bootstrap = new GrpcClient(target, { credentials: { ca: trustAnchorPem } });
const identity  = await enroll(bootstrap);           // over the tls-only channel
await bootstrap.upgrade({ ...identity, ca: trustAnchorPem });  // now mTLS

3. Protocol conformance notes

Things that are easy to get wrong, and what this implementation does about them:

  • A DATA frame boundary is not a message boundary. Both directions accumulate and only emit complete messages. The naive version works until a message crosses a frame boundary.
  • grpc-timeout has six unitsH M S m u n. Omitting n silently drops the deadline of any client that uses it, turning a bounded call into an unbounded one.
  • Trailers-only responses. A call that fails before producing a message replies with a single HEADERS frame carrying grpc-status. A client that reads the status only from the trailers event misses every fast failure, so this client reads both.
  • Message size limits are enforced from the declared length prefix, before the body is buffered — otherwise a peer can name a 4 GiB length and force the allocation the limit was meant to prevent. Over-limit is RESOURCE_EXHAUSTED.
  • Compression is per message. A payload that grows under gzip is sent raw; a message compressed with an unnegotiated algorithm is UNIMPLEMENTED, not a decode error.
  • grpc-message percent-encoding follows the spec grammar (%x20-%x24 / %x26-%x7E unescaped). encodeURIComponent over-escapes; it round trips, but it is not the grammar.
  • Unexpected handler throws become INTERNAL. Passing an arbitrary runtime error through as, say, NOT_FOUND would make the caller's retry logic actively wrong.
  • Health checks report an unknown service as NOT_FOUND from Check (so a probe can tell "misconfigured" from "down") but as SERVICE_UNKNOWN from Watch, per the spec.
  • Reflection answers list_services on presence of the field, not on it being non-empty — clients legitimately send "" — and lists the well-known services alongside user services.

4. Streaming

All four kinds are supported. Bidi handlers own their own termination — the framework does not end the call when the handler's promise resolves, because a bidi handler typically returns as soon as it has attached its listeners, and ending there would close the stream before the first message was exchanged.

app.registerController('DatabaseService', {
  Watch: {
    kind: 'server_stream', requiresAuth: true, minSecurityLevel: 'mtls',
    request:  [{ no: 1, name: 'collection', type: 'string' }],
    response: [{ no: 1, name: 'payloadJson', type: 'string' }],
    handler: async (req, call) => {
      const unsubscribe = hub.subscribe(req.collection, (e) => {
        if (call.isActive()) call.write({ payloadJson: JSON.stringify(e) });
      });
      await new Promise((resolve) => call.on('cancelled', resolve));
      unsubscribe();
    },
  },
});

call.isActive() is how a producing handler notices cancellation without waiting for a write to fail.

5. Graceful shutdown

app.health.shutdown();          // report NOT_SERVING so load balancers drain us
await app.close({ graceMs: 10000 });

close() stops accepting, lets in-flight streams finish within the grace period, then sends GOAWAY and closes the sessions. Draining first is what makes it graceful; closing the sessions afterwards is what makes it actually terminate — http2.Server#close() on its own waits forever for a keep-alive client that never disconnects.

6. Relationship to @fitfak/database

@fitfak/database ships its own small gRPC layer so that it installs with zero dependencies and can be used standalone. When both packages are present, mount the database services onto an existing application instead of running a second listener:

const { createDatabaseServer } = require('@fitfak/database');
const dbServer = createDatabaseServer({ /* ... */ });
dbServer.attachTo(app);   // shares this process's port, auth and middleware