@megarouter/sdk
v0.4.0
Published
UI-agnostic Megarouter API client and model form planning runtime.
Maintainers
Readme
@megarouter/sdk
UI-agnostic runtime for downstream Megarouter integrations.
The package has two jobs:
- Fetch Megarouter discovery data from
/v1/logical_modelsand schema endpoints. - Turn a Megarouter model schema plus current values into a
FormPlanthat 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/sdkRequires 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:
- Run a backend proxy that holds the real
MEGAROUTER_API_KEY. - Have the browser talk to your proxy with
auth: { type: "cookie" }(or your own session token). - 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=/v1Then:
const client = new MegarouterClient();Configuration priority is (highest first):
- Explicit options passed to
new MegarouterClient(options) - Process-wide
configureMegarouter(options) - Environment variables (
MEGAROUTER_*, plusNEXT_PUBLIC_MEGAROUTER_BASE_URLas a fallback for the base URL only) - 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 afterapplies_to, tier, and dynamic visibility are evaluated.groups: UI-neutral grouping, currentlyprimaryandadvanced.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.
