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

@megarouter/sdk

v0.4.0

Published

UI-agnostic Megarouter API client and model form planning runtime.

Readme

@megarouter/sdk

UI-agnostic runtime for downstream Megarouter integrations.

The package has two jobs:

  • Fetch Megarouter discovery data from /v1/logical_models and schema endpoints.
  • Turn a Megarouter model schema plus current values into a FormPlan that any UI can render.

It does not import React, DOM components, Tailwind, shadcn, or any design system.

Install

npm install @megarouter/sdk
# or: pnpm add @megarouter/sdk
# or: yarn add @megarouter/sdk

Requires a running Megarouter relay and an API key (mint one from the console). Set baseUrl to your relay origin and pass the key as apiKey. Never ship the key to a browser — see "Security" below.

Basic usage

import {
  MegarouterClient,
  buildGenerationPayload,
  unsafeDynamicEvaluator,
} from "@megarouter/sdk";

const client = new MegarouterClient({
  baseUrl: "https://api.example.com",
  apiKey: process.env.MEGAROUTER_API_KEY,
});

const plan = await client.getLogicalModelFormPlan(
  "gpt-image-2",
  { prompt: "a quiet studio product photo" },
  { capability: "image.generate" },
  { evaluateDynamic: unsafeDynamicEvaluator },
);

for (const field of plan.visibleFields) {
  // Render with any UI stack:
  // field.name, field.type, field.required, field.enum, field.value, ...
}

const payload = buildGenerationPayload(plan, undefined, {
  logicalModel: "gpt-image-2",
  capability: "image.generate",
  mode: "async",
});

await client.createGeneration(payload);

Catalog cache refresh

Long-lived hosts can compare the opaque catalog revision before reusing a cached schema. Treat the value only as an equality token; do not parse or sort it.

const { revision } = await client.getCatalogRevision();
const schema = await client.getLogicalModelSchema("gpt-image-2", {
  capability: "image.generate",
});

if (schema.revision !== revision) {
  // The catalog changed between the two reads; refetch before rendering.
}

An explicitly unknown schema field type is rejected instead of silently being rendered and submitted as text. Upgrade the SDK/host renderer before publishing a Catalog that introduces a new field type.

Generation templates

Templates are business-level forms that resolve to a normal Megarouter generation request on the server. Use the template endpoints for quote and submit; do not send a template code as logical_model.

import {
  MegarouterClient,
  buildTemplateResolveRequest,
  schemaFromTemplate,
} from "@megarouter/sdk";

const client = new MegarouterClient({
  baseUrl: "https://api.example.com",
  apiKey: process.env.MEGAROUTER_API_KEY,
});

const template = await client.getTemplateDefinition("product-photo-studio");
if (!template) throw new Error("template not found");

const schema = schemaFromTemplate(template);
const payload = buildTemplateResolveRequest(schema, {
  image_url: ["https://example.com/product.png"],
  resolution: "4K",
});

const quote = await client.quoteTemplate(template.code, payload);
if (!quote.margin_ok && quote.margin_policy?.mode !== "warn") {
  throw new Error("template price is below margin policy");
}

await client.createJob({
  ...payload,
  invocation: { type: "template", code: template.code },
});

createTemplateGeneration(code, payload) remains source-compatible and now delegates to the same /jobs protocol.

Server-side Node example

Use MegarouterClient from a trusted server process. The client reads MEGAROUTER_API_KEY from the environment, so no key needs to appear in application code or in a public bundle.

// scripts/preview-form.ts
import {
  MegarouterClient,
  buildGenerationPayload,
  unsafeDynamicEvaluator,
} from "@megarouter/sdk";

// Reads MEGAROUTER_API_KEY and MEGAROUTER_BASE_URL from process.env.
const client = new MegarouterClient();

const plan = await client.getLogicalModelFormPlan(
  "gpt-image-2",
  { prompt: "soft window light on a ceramic mug" },
  { capability: "image.generate" },
  { evaluateDynamic: unsafeDynamicEvaluator },
);

const payload = buildGenerationPayload(plan, undefined, {
  logicalModel: "gpt-image-2",
  capability: "image.generate",
  mode: "async",
});

const job = await client.createGeneration(payload);
console.log("submitted job", job);

Next.js webhook receiver

Megarouter can POST terminal job events to JobSpec.callback_url. In a Next.js App Router project, keep the protocol handling in one server-only module and re-export the handler from the route file:

// app/api/megarouter/webhook/megarouter.ts
import { MegarouterWebhook, createMemoryReplayStore } from "@megarouter/sdk/next";

export const { POST } = MegarouterWebhook({
  secret: process.env.MEGAROUTER_WEBHOOK_SECRET!,
  replayStore: createMemoryReplayStore(), // development only
  async onSucceeded(event) {
    await markJobSucceeded(event.job_id, event.result);
  },
  async onFailed(event) {
    await markJobFailed(event.job_id, event.error);
  },
});
// app/api/megarouter/webhook/route.ts
export { POST } from "./megarouter";

The helper verifies X-Megarouter-Signature, X-Megarouter-Timestamp, and X-Megarouter-Nonce over the raw request body before user code runs. It does not initialize or migrate your application database. Store your own job state in onSucceeded / onFailed, and use a durable replayStore backed by Redis, Postgres, or your existing database in production. The built-in memory replay store is only for local development and single-process tests.

You can also register the receiver URL with Megarouter once from server-side startup or deployment code. After registration, createGeneration calls may omit callback_url; Megarouter applies the tenant's registered default endpoint unless a request supplies an explicit callback URL.

import { MegarouterClient } from "@megarouter/sdk";

const client = new MegarouterClient({
  baseUrl: process.env.MEGAROUTER_BASE_URL!,
  auth: { type: "api_key", apiKey: process.env.MEGAROUTER_API_KEY! },
});

await client.registerWebhookEndpoint(process.env.MEGAROUTER_WEBHOOK_URL!);

Registration does not rotate or fetch your signing secret. Use rotateWebhookSecret() deliberately when provisioning a new receiver, store the returned secret in your own secret manager, and pass that value to MegarouterWebhook.

Non-React / framework-free usage

buildFormPlan is the runtime's only required entry point. Anything that can fetch a schema (Node script, CLI, Vue setup function, Svelte store, mini-program shell) can drive it.

// scripts/print-form.ts
import { buildFormPlan, applyFormChange } from "@megarouter/sdk";

const schema = await fetch("https://api.example.com/v1/logical_models/gpt-image-2/schema?audience=full")
  .then((r) => r.json())
  .then((body) => body.schema);

let plan = buildFormPlan(
  schema,
  { prompt: "a quiet studio product photo" },
  { capability: "image.generate" },
);

for (const field of plan.visibleFields) {
  const status = field.required ? "required" : "optional";
  const value = plan.values[field.name];
  console.log(`- ${field.name} (${field.type}, ${status}): ${JSON.stringify(value)}`);
}

// Mutating a value re-runs the same pipeline:
plan = applyFormChange(schema, plan.values, "size", "large", { capability: "image.generate" });
console.log("after change:", plan.values);

if (plan.errors.length > 0) {
  for (const err of plan.errors) console.error(`! ${err.field}: ${err.message}`);
}

There is no DOM, no React, and no styling involved. The host can pipe the same plan.fields array into any renderer it owns.

Security: never ship MEGAROUTER_API_KEY to a browser

MegarouterClient will read MEGAROUTER_API_KEY from process.env when running in Node. It will not read NEXT_PUBLIC_MEGAROUTER_API_KEY — that variable is intentionally not supported, because anything prefixed NEXT_PUBLIC_ is shipped in browser bundles.

For browser apps:

  1. Run a backend proxy that holds the real MEGAROUTER_API_KEY.
  2. Have the browser talk to your proxy with auth: { type: "cookie" } (or your own session token).
  3. Have the proxy attach the Megarouter API key to outbound requests, enforce per-user permissions, and decide pricing.

Concrete React + proxy examples live in @megarouter/react so this package can stay framework-free.

Configuration

Server-side default configuration reads environment variables:

MEGAROUTER_API_KEY=mr_xxx
MEGAROUTER_BASE_URL=https://api.megarouter.ai
MEGAROUTER_ENV=production
MEGAROUTER_API_SURFACE=v1
MEGAROUTER_API_PREFIX=/v1

Then:

const client = new MegarouterClient();

Configuration priority is (highest first):

  1. Explicit options passed to new MegarouterClient(options)
  2. Process-wide configureMegarouter(options)
  3. Environment variables (MEGAROUTER_*, plus NEXT_PUBLIC_MEGAROUTER_BASE_URL as a fallback for the base URL only)
  4. Built-in defaults

NEXT_PUBLIC_MEGAROUTER_API_KEY is not read. Browser bundles must not contain a Megarouter API key — use a backend proxy instead (see Security below).

Environment variable details:

| Var | Default | Notes | |---|---|---| | MEGAROUTER_API_KEY | unset | Server-only API key used when no explicit auth option is passed. | | MEGAROUTER_BASE_URL | environment default | Preferred API origin for server-side usage. | | NEXT_PUBLIC_MEGAROUTER_BASE_URL | unset | Public fallback for base URL only; never use it for secrets. | | MEGAROUTER_ENV | production | Selects the built-in base URL when no base URL is provided. Supported runtime defaults are production, sandbox, and local. | | MEGAROUTER_API_SURFACE | v1 | Selects the default API surface. Use console only for trusted server-side integrations that intentionally target /console. | | MEGAROUTER_API_PREFIX | /v1 or /console | Overrides the request path prefix after MEGAROUTER_API_SURFACE is resolved. | | MEGAROUTER_SDK_CACHE | on (off when NODE_ENV=development) | Enables the in-memory TTL cache for idempotent reference reads. Accepts truthy/falsy values; explicit client cache option wins over this. | | MEGAROUTER_SDK_CACHE_TTL | built-in default | Cache time-to-live in milliseconds. Overrides the default TTL when the cache is enabled. | | NODE_ENV | unset | Standard Node environment. When set to development the reference-read cache defaults to off so local dev sees fresh data. |

Tests and host apps that need to reset the process-wide configuration can call resetMegarouterConfig().

Browser integrations should not expose API keys. Use a host backend proxy:

const client = new MegarouterClient({
  baseUrl: "/api/megarouter",
  auth: { type: "cookie" },
});

The host backend proxy should inject the Megarouter API key and enforce the host application's user permissions.

Runtime contract

buildFormPlan(schema, values, context, options) returns:

  • fields: every schema field, including hidden fields.
  • visibleFields: renderable fields after applies_to, tier, and dynamic visibility are evaluated.
  • groups: UI-neutral grouping, currently primary and advanced.
  • values: input values after hidden cleanup and default application.
  • errors: UI-neutral validation issues.
  • changed: notes such as default application or hidden field cleanup.

The downstream app owns rendering, upload widgets, localization, styling, and state storage.

Resource-backed media

Media fields accept full MediaAsset[] values. A URL string is still accepted for legacy compatibility, but reusable inputs should carry a ResourceItem id:

import {
  MegarouterClient,
  isResourceBackedMediaAsset,
  mediaAssetsToResourceIds,
  normalizeMediaAsset,
  resourceViewToFileInfoItem,
  resourceViewToMediaAsset,
} from "@megarouter/sdk";

const client = new MegarouterClient({ baseUrl: "/api/megarouter", auth: { type: "cookie" } });

const resources = await client.listResources({
  permission: "premium",
  downstream_subject_id: "user_123",
  source_kind: "generated",
  result_type: "image",
});
const picked = normalizeMediaAsset({
  id: resources[0].id,
  url: resources[0].urls.view,
  thumbnail_url: resources[0].urls.preview,
  source: "resource",
});

console.log(isResourceBackedMediaAsset(picked)); // true
console.log(mediaAssetsToResourceIds([picked])); // [resources[0].id]

When a downstream page embeds an existing file-centric widget, do not make that widget's FileInfoItem shape the business contract. Adapt a ResourceItem view at the component edge and keep metadata.resourceItemId for submit:

const resource = await client.getResource("res_123");
const mediaAsset = resourceViewToMediaAsset(resource);
const fileInfo = resourceViewToFileInfoItem(resource);

// Use fileInfo for a legacy file-card/cropper/uploader component.
// Use mediaAsset or fileInfo.metadata.resourceItemId for generation submit.

For direct uploads, Megarouter exposes the ResourceItem upload flow through the same client. The tenant must have a default storage bucket with public_base_url configured so the resulting ResourceItem has a durable view URL:

const { resource, upload } = await client.createResourceUpload({
  result_type: "image",
  downstream_subject_id: "user_123",
  downstream_subject_type: "user",
  source_kind: "upload",
  tags: ["project:abc", "avatar"],
  filename: file.name,
  mime_type: file.type || "application/octet-stream",
  bytes: file.size,
});

await fetch(upload.url, {
  method: upload.method,
  headers: upload.headers,
  body: file,
});

const { resource: ready } = await client.completeResourceUpload(resource.id);

Remote URLs should go through URL ingest when the asset needs to become reusable or searchable. mode: "external" stores the URL as the ResourceItem's file object after Megarouter verifies that the URL is safe and compatible with the declared result type:

const { resource } = await client.ingestResourceURL({
  result_type: "image",
  source: { url: "https://example.com/input.png" },
  mode: "external",
  downstream_subject_id: "user_123",
  source_kind: "external_url",
  tags: ["project:abc"],
});

Use mode: "external" to keep the URL as a checked external reference. mode: "rehost" is reserved for a future server-side copy flow and currently returns 501 rehost_not_implemented; use direct uploads when Megarouter must own the bytes today.

React and other UIs

React is supported through a separate adapter package:

import { useMegarouterForm } from "@megarouter/react";

const form = useMegarouterForm({ schema, context: { capability } });

The adapter returns form state and change handlers. The host app still provides the actual FieldRenderer, so shadcn, MUI, Ant Design, custom design systems, Vue, Svelte, and native inputs can all consume the same runtime contract.

Without React, call buildFormPlan and applyFormChange directly.

Dynamic expressions

Megarouter schemas may include ParamSchema.dynamic JS expressions. The server stores them but never executes them.

This runtime exports unsafeDynamicEvaluator as an opt-in helper. Use it only with schemas from a trusted Megarouter instance. Browser integrations that need strong isolation can pass their own evaluator through:

buildFormPlan(schema, values, context, {
  evaluateDynamic: mySandboxedEvaluator,
});

If no evaluator is provided, dynamic expressions are ignored and static schema fields are used.

Pricing

The runtime can derive a basic display estimate from schema.pricing:

const plan = buildFormPlan(schema, values, context);
console.log(plan.pricing);

Downstream platforms with their own credits, subscriptions, discounts, or regional prices should use their own server-side quote service. The SDK exposes a pricing adapter shape, but final billing must be decided server-side.