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

@syncanix/sdk-node

v0.1.0

Published

Syncanix Node SDK — install + syncanix.init() in your app, get framework registry discovery + drift detection + runtime tool registration.

Readme

@syncanix/sdk-node

The Syncanix Node SDK. Install it in your customer-facing API, call syncanix.boot() at startup, and Syncanix knows what your app exposes — so the agent loop can call your tools, drift-detect new endpoints, and verify intents before honouring sensitive requests.

npm install @syncanix/sdk-node

Requires Node ≥ 20.

What the SDK does

  1. Discovers your live HTTP routes by walking your framework's runtime route registry (NestJS / Express / Fastify) — no static scan, no source-file shipping.
  2. Uploads the resulting capability catalog to the Syncanix server (with idempotent ETag short-circuit so re-boots cost a 304).
  3. Detects drift at runtime via Node's diagnostics_channel — new endpoints called in production that aren't in the catalog get reported back to Syncanix.
  4. Registers SDK-native tools (syncanix.tool({...})) that the LLM agent can invoke without a public HTTP route.
  5. Verifies inbound intents — when the agent invokes a tool/route, the Syncanix server signs an HMAC envelope; the SDK middleware (Express) or guard (NestJS) checks it before the handler runs.

The customer pattern

Every integration follows the same four-step shape:

   walker → boot → drift subscriber → verify-intent middleware

The sections below show this pattern realised in NestJS, Express, and Fastify.

Quick start — NestJS

// src/main.ts
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { DiscoveryService } from '@nestjs/core';
import { boot, subscribeToHttpChannel, walkNestJS } from '@syncanix/sdk-node';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);

  // 1 — walk the NestJS controller registry
  const discovery = app.get(DiscoveryService);
  const capabilities = walkNestJS(discovery);

  // 2 — boot: upload catalog + measure overhead (<30 ms target)
  const result = await boot({
    apiKey: process.env.SYNCANIX_API_KEY!,
    capabilities,
    frameworks: ['nestjs'],
    repoRoot: process.cwd(),
    scannerVersion: '@syncanix/[email protected]',
  });
  console.log(`[syncanix] boot ${result.bootDurationMs.toFixed(1)}ms`);

  // 3 — subscribe to runtime drift (optional)
  subscribeToHttpChannel({
    capabilities,
    onDrift: (event) => {
      console.warn('[syncanix] drift:', event.type, event.method, event.path);
    },
  });
}
bootstrap();

Add the verify-intent guard to any controller that handles agent-invoked traffic:

// src/tools/tools.module.ts
import { Module, UseGuards } from '@nestjs/common';
import { createVerifyIntentNestGuard } from '@syncanix/sdk-node';
import { ToolsController } from './tools.controller';

const VerifyIntent = createVerifyIntentNestGuard({
  secret: process.env.SYNCANIX_INTENT_SECRET!,
});

@UseGuards(VerifyIntent)
@Module({ controllers: [ToolsController] })
export class ToolsModule {}

The guard throws an IntentVerificationError on failure. Add a Nest ExceptionFilter to convert it to a 403 response with the failure reason in the body.

Quick start — Express

// src/server.ts
import express from 'express';
import {
  boot,
  createVerifyIntentExpressMiddleware,
  subscribeToHttpChannel,
  walkExpress,
} from '@syncanix/sdk-node';
import { router } from './routes';

const app = express();
app.use(router);

// 1 — walk Express's runtime route registry
const capabilities = walkExpress(app);

// 2 — boot
await boot({
  apiKey: process.env.SYNCANIX_API_KEY!,
  capabilities,
  frameworks: ['express'],
  repoRoot: process.cwd(),
  scannerVersion: '@syncanix/[email protected]',
});

// 3 — drift detector
subscribeToHttpChannel({
  capabilities,
  onDrift: (event) => console.warn('[syncanix] drift:', event),
});

// 4 — verify-intent middleware on agent-invoked routes
app.use(
  '/tools',
  createVerifyIntentExpressMiddleware({
    secret: process.env.SYNCANIX_INTENT_SECRET!,
  }),
);

app.listen(3000);

The middleware responds 403 {error: "forbidden", reason: "..."} on failure. On success, the verified payload is attached to req.syncanixIntent.

Quick start — Fastify

// src/server.ts
import Fastify from 'fastify';
import {
  boot,
  subscribeToHttpChannel,
  walkFastify,
  type FastifyRouteOptionsLike,
} from '@syncanix/sdk-node';

const fastify = Fastify();

// 1 — collect routes via Fastify's documented onRoute hook
const routes: FastifyRouteOptionsLike[] = [];
fastify.addHook('onRoute', (opts) => routes.push(opts));

// ... register your routes (fastify.get(...), fastify.post(...), etc.)

await fastify.ready();

// 2 — walk the collected routes
const capabilities = walkFastify(routes);

// 3 — boot
await boot({
  apiKey: process.env.SYNCANIX_API_KEY!,
  capabilities,
  frameworks: ['fastify'],
  repoRoot: process.cwd(),
  scannerVersion: '@syncanix/[email protected]',
});

// 4 — drift detector
subscribeToHttpChannel({
  capabilities,
  onDrift: (event) => console.warn('[syncanix] drift:', event),
});

await fastify.listen({ port: 3000 });

SDK-native tools

For agent-invocable logic that does not live as a public HTTP route, use createToolRegistry():

import { boot, createToolRegistry } from '@syncanix/sdk-node';

const registry = createToolRegistry();

registry.tool({
  name: 'refund',
  description: 'Refund a customer order.',
  handler: async (args, ctx) => {
    const { orderId, amount } = args as { orderId: string; amount: number };
    // ... your refund logic, scoped to ctx.tenantId
    return { ok: true };
  },
});

// Compose tools alongside walker output
await boot({
  apiKey: process.env.SYNCANIX_API_KEY!,
  capabilities: [...walkerCapabilities, ...registry.getRegisteredCapabilities()],
  frameworks: ['express', 'sdk-tool'],
  repoRoot: process.cwd(),
  scannerVersion: '@syncanix/[email protected]',
});

// Invoke at agent-loop time
const result = await registry.invoke(
  'refund',
  { orderId: 'o_1', amount: 50 },
  {
    tenantId: 't_123',
  },
);

Tool name regex: ^[a-z][a-z0-9-]*$. Hyphens allowed; underscores rejected. Names must be unique per registry — duplicate registration throws.

API key resolution

boot() (via init()) resolves the API key in this priority order:

  1. The apiKey option passed to boot({...}).
  2. The SYNCANIX_API_KEY environment variable.
  3. ~/.syncanix/credentials — convenience for local dev.

Production callers should always set SYNCANIX_API_KEY via your secrets manager (Secrets Manager → CDK → Lambda env var).

Drift detection

subscribeToHttpChannel listens to Node's built-in 'http.server.request.start' channel. For every request whose {method, path} does not match any known capability, the onDrift handler fires once (deduped on <METHOD>:<path> for the lifetime of the subscriber).

Common pattern: buffer drift events and flush every minute via uploadDriftEvents:

import { subscribeToHttpChannel, uploadDriftEvents, type DriftEvent } from '@syncanix/sdk-node';

const buffer: DriftEvent[] = [];
subscribeToHttpChannel({
  capabilities,
  onDrift: (event) => buffer.push(event),
});

setInterval(async () => {
  if (buffer.length === 0) return;
  const batch = buffer.splice(0, buffer.length);
  await uploadDriftEvents({
    apiKey: process.env.SYNCANIX_API_KEY!,
    events: batch,
  });
}, 60_000).unref();

Send notifications

notify() tells an end user a background process finished — even when the chat widget is closed. The widget surfaces it as a launcher badge and, on open, as an assistant message they can reply to. It is one authenticated POST /v1/notifications (secret gak_ key), validated against the shared contract before it leaves your process.

import { notify } from '@syncanix/sdk-node';

await notify({
  apiKey: process.env.SYNCANIX_API_KEY!, // gak_… (secret, server-side only)
  endUserId: 'user_123', // YOUR stable id for the human
  kind: 'deploy.finished', // machine-readable slug
  title: 'Your deploy finished', // localize to the user's language
  body: 'Build #1820 is live in production.',
  action: { label: 'View deploy', url: 'https://app.example.com/deploys/1820' },
  dedupeKey: 'deploy-1820', // optional — retries collapse to one
});
// → { id: '…', created: true }   (created:false when reused via dedupeKey)

title/body are rendered verbatim — localize them before sending. Supply a dedupeKey for anything you might send twice (retried jobs, at-least-once queues); the call is then idempotent and safe to retry. A bad call fails fast client-side with a typed NotifyValidationError before any network I/O. Full contract + non-JS snippets: Send closed-widget notifications.

Live endpoint reporting (witness reporter)

Where drift detection reports only new endpoints, the witness reporter reports every endpoint your server actually serves — method, path template, and the inferred request/response shape (never raw bodies) — so the catalog's runtime-observed surface stays live. It's the dd-trace-style "exporter": the @syncanix/api-witness middleware observes each request, and the reporter batches those observations and ships them to the catalog via the submitWitnessBatch API, authenticated by your API key.

One-call drop-in for Express:

import express from 'express';
import { installExpressWitnessReporter } from '@syncanix/sdk-node';

const app = express();
app.use(express.json());

// Resolves the API key (option → SYNCANIX_API_KEY → ~/.syncanix/credentials),
// installs the witness middleware, and starts the background reporter.
const reporter = await installExpressWitnessReporter(app, { environment: 'production' });

// ...your routes...

// graceful shutdown (e.g. in your SIGTERM handler):
process.on('SIGTERM', () => void reporter.close());

Lower-level: wire the middleware and reporter yourself.

import { createWitnessMiddleware } from '@syncanix/api-witness';
import { createWitnessReporter } from '@syncanix/sdk-node';

const reporter = createWitnessReporter({ apiKey: process.env.SYNCANIX_API_KEY! });
app.use(createWitnessMiddleware({ onWitness: (w) => reporter.record(w) }));

Serverless (Lambda)

A frozen Lambda can't run a background timer between invocations, so disable it and flush per invocation:

const reporter = createWitnessReporter({ apiKey, flushIntervalMs: 0 });
// ...inside the handler, after observing...
await reporter.flush();

Notes

  • Never breaks the host app. A failed flush re-queues (bounded by maxQueueSize) and is surfaced via onError; it never throws into the request path. The background timer is unref()'d so it never keeps a process alive.
  • Deduped at the server's granularity (method + pathTemplate): colliding observations merge their shapes and keep a non-error status over an error one.
  • Environment is derived from the API key (gak_<env>_…); passing an environment that disagrees with the key warns and uses the key's.

Verify-intent envelope

When the Syncanix LLM agent decides to call one of your tools/routes, the server issues a short-lived signed envelope:

header value: X-Syncanix-Intent: base64url(JSON({ payload, signature }))
  payload = { toolCallId, tenantId, userId?, method, path, issuedAt, expiresAt }
  signature = hex(HMAC-SHA256(JSON(payload), SYNCANIX_INTENT_SECRET))

The middleware/guard verifies:

  • HMAC signature matches (constant-time compare).
  • The intent has not expired (now <= expiresAt).
  • The inbound request's {method, path} matches the signed pair (no replay against a different route).

Failure responses include a stable reason enum (missing-header, malformed, bad-signature, expired, method-mismatch, path-mismatch) so failures are debuggable from CloudWatch logs without leaking token contents.

Boot overhead

boot() targets a <30 ms total overhead (init + catalog build + upload). The function emits a one-line console.warn if the budget is exceeded, and returns per-stage timings for diagnostics:

const result = await boot({ ... });
result.bootDurationMs;     // total (ms)
result.initDurationMs;     // API-key resolution
result.catalogDurationMs;  // build + upload

Customer infrastructure (cold Lambda, container start, slow CI) may exceed 30 ms legitimately. Raise the budget per-call rather than tolerate noise:

await boot({ ..., budgetMs: 200 });

What the SDK does not do

  • No static source scan. The CLI (syncanix init / syncanix scan) covers that surface. The SDK observes only what the live framework registry exposes at runtime.
  • No handler-source extraction. Function.prototype.toString returns compiled JS that is unhelpful for the LLM enrichment pipeline. The CLI's static scan populates handler text when the same capability is re-scanned at deploy time.
  • No persistent global state. Every entry point is a pure function — Lambda-friendly. Drift detection is the only stateful surface, and it's bounded by the subscriber's lifetime + the in-memory dedup set.

License

See repository root.