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

@restormel/keys

v0.2.14

Published

Drop-in BYOK for AI apps. Headless key resolution, cost estimation, and provider adapters (OpenAI, Anthropic, Google).

Readme

@restormel/keys

Drop-in BYOK for AI apps. Headless key resolution, cost estimation, and optional server helpers.

Install

npm install @restormel/keys
# or
pnpm add @restormel/keys
# or
yarn add @restormel/keys

Requirements: Node 18+, ESM.

Quick start

1. Create a Keys instance

import { createKeys, openaiProvider, anthropicProvider } from "@restormel/keys";

const keys = createKeys(
  {
    routing: { defaultProvider: "openai" },
    plans: [{ id: "default", entitlements: { allowedModels: ["*"] } }],
  },
  { providers: [openaiProvider, anthropicProvider] }
);

2. Resolve which key to use

const resolved = await keys.resolve("openai", "gpt-4o");
// { provider: "openai", model: "gpt-4o", source: "byok" | "platform", keyId?: string }

3. Estimate cost

const cost = keys.estimateCost("gpt-4o-mini");
// { modelId, providerId, inputPerMillion, outputPerMillion, unit }

4. Check entitlements

const allowed = keys.entitlements.check("gpt-4o").allowed;
const available = keys.entitlements.getAvailableModels(keys.getAllModelIds());

5. Optional: storage and server helpers

  • Storage: import { createMemoryStorage } from "@restormel/keys/storage/memory" or use encrypted localStorage with @restormel/keys/storage/encrypted-local.
  • Server: import { createMiddleware, createResolveMiddleware } from "@restormel/keys/server" for BYOK and resolve flows (standard Request/Response). If you already use a gateway (OpenRouter/Portkey/Vercel AI Gateway) or call providers directly, you keep that execution layer.
  • Optional execution helper: createProxy exists for edge cases where you explicitly want a Request/Response forwarder in your own app. It is not required and is not the default product shape.
  • Security: import { createApiKey, hashApiKey, maskApiKey, createKeyVerifier } from "@restormel/keys/security" for key hashing and verification.

Never log or store raw API keys; use hashed keys and masked display only.

6. Dashboard API client (resolve and evaluate)

If your app calls the Restormel dashboard REST API (route resolve, policy evaluate), use the typed client so you get structured errors instead of parsing JSON by hand:

import {
  resolve,
  evaluatePolicies,
  isPolicyBlocked,
  isNoRoute,
} from "@restormel/keys/dashboard";

// Resolve: returns { ok: true, data } or { ok: false, status, error, violations? }
const result = await resolve({
  projectId: "proj-1",
  environmentId: "prod",
  auth: { type: "bearer", token: process.env.RESTORMEL_GATEWAY_KEY! },
});
if (result.ok) {
  console.log(result.data.providerType, result.data.modelId);
} else if (isPolicyBlocked(result)) {
  console.error("Blocked:", result.violations);
} else if (isNoRoute(result)) {
  console.error("No route");
}

// Evaluate: returns { allowed, violations }; throws on HTTP error
const { allowed, violations } = await evaluatePolicies({
  projectId: "proj-1",
  environmentId: "prod",
  modelId: "gpt-4o",
  providerType: "openai",
  auth: { type: "bearer", token: process.env.RESTORMEL_GATEWAY_KEY! },
});

Security: Use the dashboard client only on the server. Never send the Gateway Key to the browser or expose it in client-side code. Pass the token from environment variables or a secure server session.

Canonical provider/model feed (for existing integrations): replace local provider presets with fetchCanonicalCatalog from @restormel/keys/dashboard.

import {
  fetchCanonicalCatalogWithFallback,
  filterCanonicalCatalogForViability,
  type CanonicalCatalogResponse,
} from "@restormel/keys/dashboard";

const localFallback: CanonicalCatalogResponse = {
  contractVersion: "local-fallback.v1",
  source: "restormel-keys",
  generatedAt: new Date().toISOString(),
  providers: [],
  data: [],
  paging: { limit: 0, offset: 0, count: 0 },
};

const { catalog, source, degradedReason } = await fetchCanonicalCatalogWithFallback({
  baseUrl: process.env.RESTORMEL_KEYS_BASE, // optional; defaults to env / restormel.dev
  fallback: () => localFallback,
});

const viableCatalog = filterCanonicalCatalogForViability(catalog, {
  knownRetiredModelIds: ["claude-3-5-haiku-20241022"], // optional emergency override
});

// source === "restormel" when canonical feed is live
// source === "fallback" when feed is unavailable
// degradedReason explains the fallback trigger

The endpoint is GET /keys/dashboard/api/catalog and returns a versioned contract (contractVersion), provider validation metadata (including optional validation.defaultApiBaseUrl when mode === "openai_compatible" and requiresBaseUrl === false), model variants, optional per-model deprecationDate / retirementDate (ISO) / replacementModelId (contract v6+), and externalSignals.freshness (contract v5+) with allFresh and per-signal isFresh / ageMs vs SLO maxAgeMs for degrading UI when runtime samples are stale.

Options: fetchCanonicalCatalog({ limit, offset, lifecycleState, family, includeUnhealthy: true }) — the last maps to includeUnhealthy=1 for deprecated models and non-available variants (operators only).

Crowd observations (contract 2026-03-25.catalog.v4+): when your app calls a vendor and receives a clear deprecated/retired signal, report it server-side with reportCatalogModelObservation (Bearer Gateway Key, management key, or session). Aggregated counts surface on each catalog variant as crowdObservations so all downstream apps can see community-reported signals.

Public integration guide: Canonical model & provider catalog (step-by-step for partners).

For a low-touch upgrade across existing apps, run:

npx @restormel/[email protected] patch

Batch policy filter (server-side allowed-models): Use filterModelsByPolicy to evaluate many (providerType, modelId) pairs in parallel and get per-model status: allowed, blocked_by_policy, restormel_degraded, or unknown_or_unavailable. Helpers for UI:

  • candidatesFromProviderDefinitions(providers) — flatten defaultProviders (or a subset) into candidates.
  • groupedModelsForModelSelector(sourceProviders, entries) — grouped provider + model rows with status metadata for custom pickers.
  • policyAvailabilityMapFromEntries(entries) — map keyed providerId:modelId for @restormel/keys-svelte ModelSelector policyAvailability prop. Rows include enforcement (hard for policy blocks, soft for degraded/unknown checks) so transient failures do not permanently suppress resolve retries.
  • filterProviderDefinitionsByAllowedPolicy(sourceProviders, entries) — strict allowlist: full ProviderDefinition[] with only policy-allowed models.

7. Custom provider definitions

Restormel ships first-party definitions for 15 providers, each with an expanded model list so users get a click-and-select experience with minimal setup:

  • OpenAI — gpt-4o family, gpt-4.1, o1 / o3-mini, etc. (legacy GPT-3.5 / GPT-4 Turbo ids removed from defaults)
  • Anthropic — Claude 3.5 Sonnet/Haiku, Claude 3 Opus/Sonnet/Haiku, Claude 4 family
  • Google — Gemini 2.5/2.0/1.5 Pro and Flash variants
  • xAI (Grok) — grok-3, grok-2, vision models
  • Mistral — mistral-large/medium/small, codestral, pixtral, minstral
  • Groq — Llama 3.3/3.2/3.1, Mixtral, Gemma2, Llama Guard
  • DeepSeek — deepseek-chat, reasoner, coder, r1
  • Cohere — Command R+, R, R7B, Command Light/A, Aya 23
  • Perplexity — sonar-pro, sonar, sonar-deep-research, sonar-reasoning-pro, sonar-small/medium-chat
  • Together — Llama 3.3/3.2/3.1, Mixtral, Qwen 2.5, DeepSeek V3/R1, Gemma 2, Hermes 3
  • Fireworks — Llama v3, Mixtral, Qwen 2.5, DeepSeek R1 distill, Code Llama, Falcon 2
  • Voyage AI — voyage-3, voyage-large-2, voyage-code-2 (embeddings)
  • Azure OpenAI — gpt-4o, gpt-4-turbo, gpt-35-turbo, o1, embeddings
  • OpenRouter — Curated list (openai/gpt-4o, anthropic/claude-3.5-sonnet, google/gemini-2.0-flash-exp, etc.)
  • Portkey — Common gateway-routed model ids

One import for all: Use defaultProviders so you don’t have to list each provider manually:

import { createKeys, defaultProviders } from "@restormel/keys";

const keys = createKeys(config, { providers: defaultProviders });

KeyManager and ModelSelector can then use providers={defaultProviders} (or the same array) for a full click-and-select UI. If your app uses a provider not in that list (or needs stricter custody), define your own:

import { defineProvider } from "@restormel/keys";
import type { ProviderDefinition } from "@restormel/keys";

const myProvider: ProviderDefinition = defineProvider({
  id: "my-provider",
  name: "My Provider",
  models: ["my-model-1", "my-model-2"],
  aliases: ["my-alias"],
  icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">...</svg>',
  async validateKey(credential, fetchFn = fetch) {
    const res = await fetchFn("https://api.my-provider.com/v1/models", {
      headers: { Authorization: `Bearer ${credential}` },
    });
    if (!res.ok) return { valid: false, errors: [`${res.status}`] };
    return { valid: true };
  },
  estimateCost(modelId) {
    return { id: modelId, inputPerMillion: 1, outputPerMillion: 3, unit: "USD" };
  },
  createClient(credential) {
    return { provider: "my-provider", baseUrl: "https://api.my-provider.com" };
  },
});

Pass custom providers alongside built-ins to createKeys and to UI components:

const keys = createKeys(config, {
  providers: [openaiProvider, anthropicProvider, myProvider],
});

Provider aliases: Use the aliases field for normalisation (e.g. google has aliases ["vertex", "gemini"]). The resolveProviderId(id, providers) helper finds a provider by id or alias. Use canonicalizeProviderId(id, providers) when persisting key records so that alias-based ids (e.g. "vertex") are stored as the definition's canonical id (e.g. "google"); this avoids host-specific id mismatch when the UI shows one id and storage uses another.

Storage canonicalization when migrating from alias ids: If your app historically stored provider ids under an alias (e.g. vertex for Google), you can (a) persist canonical id from now on — when saving new keys or updating, use canonicalizeProviderId(providerId, providers) and store that (e.g. google); (b) keep a thin translation layer for legacy data — when loading keys from storage, if your store still has vertex, map it to google for the UI (e.g. provider: storedProvider === "vertex" ? "google" : storedProvider) so KeyManager and ModelSelector see a known provider id; (c) optional one-time migration — backfill existing key records to canonical ids so save/revalidate/remove paths can assume canonical id in storage and you can remove the translation layer.

Custom icons: Set icon on your ProviderDefinition to an inline SVG string. UI components (KeyManager, ModelSelector) will render it instead of the built-in generic icon.

8. Server-side validation and KeyManager integration

For apps where raw credentials should not go directly from the browser to providers, use the onValidate prop on KeyManager:

// Next.js / React example
<KeyManager
  keys={keys}
  userId={userId}
  providers={providers}
  onValidate={async (provider, rawCredential) => {
    const res = await fetch("/api/validate-key", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ provider, credential: rawCredential }),
    });
    return res.json(); // { valid: boolean, errors?: string[] }
  }}
  onKeyAdded={async (key, rawCredential) => {
    const res = await fetch("/api/keys", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ provider: key.provider, credential: rawCredential }),
    });
    const data = await res.json();
    if (!res.ok) return { ok: false, error: data.error ?? "Save failed" };
    return { ok: true, savedKey: data.key };
  }}
  onKeyRemoved={async (keyId) => {
    const res = await fetch(`/api/keys/${keyId}`, { method: "DELETE" });
    if (!res.ok) return { ok: false, error: "Remove failed" };
    return { ok: true };
  }}
/>

The component awaits each host callback. On { ok: false, error }, the error is shown inline and the form stays open. On { ok: true }, the entry closes and the list refreshes.

Revalidate: Provide onRevalidate(keyId, provider) so the key detail view can re-check an existing key server-side. The component passes keyId and provider; prefer key-id-centric revalidate endpoints that accept keyId, look up the stored credential by keyId (server-side), then validate against the provider. That way revalidation is scoped to the specific key and you avoid provider-only endpoints that don't know which key to revalidate. Prefer this over overloading onValidate with an empty-credential sentinel.

Validate-then-persist (avoid double validation): If you use onValidate, the component validates before calling onKeyAdded. Your save endpoint can skip a second validation and trust the client already validated, so the add flow does not hit the provider twice. For example, have the client send preValidated: true in the save body when it used onValidate; the server then persists without calling the provider again. If you prefer defence-in-depth, validate again on persist and return { ok: false, error } (and optionally delete the just-saved key) on failure; that keeps UI consistent but duplicates the provider call.

KeyRecord metadata: Pass keys as KeyRecord[] (extends KeyConfig with id, status, validatedAt, updatedAt, lastError, fingerprint, metadata) to display richer status in the list view. Supported statuses: active, pending_validation, invalid, revoked. Set updatedAt (ISO 8601) when the record was last updated so hosts don't need to tuck it into metadata.

License

MIT