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

@foir/sdk

v0.9.0

Published

Official Foir SDK — editor iframe client + server-side platform helpers

Readme

@foir/sdk

Official Foir SDK — client-agnostic typed GraphQL transport, customer auth, progressive media, content rendering, realtime collaboration, and editor/server helpers for building apps on the Foir platform.

The SDK is split into independent subpath exports so consumers only pull what they use. Everything heavy (react, graphql, yjs, hls.js, …) is an optional peer dependency gated to the subpath that needs it.

Install

pnpm add @foir/sdk
# plus the peers for the subpaths you use, e.g.:
pnpm add graphql            # for @foir/sdk/client + @foir/sdk/auth
pnpm add react              # for @foir/sdk (editor), /media, /content
pnpm add yjs y-websocket    # for @foir/sdk/realtime

Subpath exports

| Import | What it is | Peers | | --- | --- | --- | | @foir/sdk | Editor iframe client — EditorProvider, useEditor, useAutoResize, createPlatformClient, host↔editor message protocol | react | | @foir/sdk/client | Client-agnostic GraphQL transport for SSR/edge loaders — foirRequest, isFoirPreview, absolutiseFoirMediaUrl | graphql | | @foir/sdk/data | Typed data client over /graphqlcreateFoirClient, per-key-shaped ops, include/omit, fetchFoirManifest; prints the document for you, so still no graphql | none | | @foir/sdk/auth | Framework-agnostic customer auth — login/register/OTP/provider flows, token lifecycle (SessionAdapter), createFoirSessionClient (Foir-hosted cookie sessions), authedRequest, guards | graphql | | @foir/sdk/react | Drop-in React auth — <FoirProvider> (token or hostedSession mode), useFoirSession/useFoirUser/useFoirAuth, <SignIn>, <OAuthButtons>, <UserButton> | react, graphql | | @foir/sdk/next | Next.js App Router BFF auth — createFoirCookieAdapter, getFoirSession, foirSignIn*, completeFoirProviderLogin, createFoirMiddleware | graphql | | @foir/sdk/server | Server-side helpers for extension/config API backends — createServerClient, verifyWebhookSignature, verifyScopedToken, callback client | — | | @foir/sdk/media | Progressive media — ProgressiveMedia, BlurhashCanvas, useProgressiveLoad, variant-tier adapters | react (hls.js, blurhash optional) | | @foir/sdk/content | Content-rendering primitives — createContentRenderer block dispatch + Lexical RichTextRenderer | react | | @foir/sdk/realtime | Thin collaboration helper over y-websocket — canonical room addressing, presence, token refresh | yjs, y-websocket | | @foir/sdk/codegen | Build-time graphql-codegen preset (foirCodegen) + exhaustive block-registry plugin | @graphql-codegen/cli, @graphql-codegen/client-preset (devDeps) |

Usage

Typed data client (no GraphQL, no codegen step)

@foir/sdk/data executes over the same /graphql endpoint every other consumer uses — one execution surface, one executor, one set of scope checks, one response cache. What you skip is writing the documents: the client loads the selection tree GET /v1/manifest publishes for that op, applies your include/omit, and prints one.

Printing a pre-resolved tree is string building, so the package still needs no graphql dependency at all — no graphql, no @graphql-typed-document-node/core, no @graphql-codegen/*, and no new peer.

import { createFoirClient } from '@foir/sdk/data';
// generated by `foir types` from GET /v1/manifest
import * as generated from './foir.generated';

const foir = createFoirClient({
  apiUrl: process.env.PLATFORM_API_URL!,   // a bare origin or the /graphql URL
  apiKey: process.env.PLATFORM_API_KEY!,
  generated,                               // the whole module: types, trees, version
});

const post = await foir.post.findUnique({
  naturalKey: 'hello',
  include: { author: true },   // relations opt in
  omit: ['bodyRichtext'],      // drop the heavy field
});

const posts = await foir.post.findMany({ first: 10 });   // a Relay connection
const hits  = await foir.ns.search.searchRecords({ query: 'shoes' });
const other = await foir.raw('{ designTokens { colors } }');  // any document, any root field

One import, no type parameter. generated carries both halves of what foir types writes — the FoirSchema type and the runtime selection trees the documents are printed from — so FoirSchema is inferred rather than restated, and an artifact added to the generator later costs your call site nothing. A module missing one of those exports is a compile error naming it, not a quiet degrade: pass it whole, or do not pass it.

The four fields it replaces (schemaVersion, selections, namespaceSelections, selectionUnions) still work and still win where both are given, so an existing client keeps compiling and behaving exactly as it did.

Scalars are always complete, so post.title is typed present and is present — there is no partial-select footgun. include adds a relation key, omit removes a scalar, and polymorphic fields are discriminated unions on __typename that narrow under an exhaustive switch with no casts.

The client is shaped by your credential. The generated FoirSchema contains only what that key's manifest lists, so a read-only pk_ key has no create to call:

foir.post.create({ data });   // ✗ compile error under a pk_ key — not in its manifest

generated is optional. Without it — and without selections — the client fetches GET /v1/manifest once at first use and reads the same trees off it. A fallback, so a zero-codegen start works, at the cost of a round trip in front of the first query on every cold invocation. It says so, once, when it is built that way; silence it with announceManifestFallback: false.

Reads that can be shared are sent to GET /cdn/graphql with only the persisted-query hash in the URL, so a CDN in front of you can serve the next visitor without reaching origin. That is a query, on a pk_ key, with no customer bearer, on the published channel; everything else is a POST /graphql.

foir.raw() is the escape hatch, and it takes a GraphQL document — it reaches anything the credential can, including root fields no model or namespace claims. It returns the whole data object.

Namespaces (search, customers, notifications) live under foir.ns because the client carries no runtime schema to tell a namespace from a model. ns and raw are the only reserved names.

Session-bound reads take a bearerToken, as a string or a function resolved per request:

const foir = createFoirClient({
  apiUrl,
  apiKey,
  generated,
  bearerToken: () => session.get('foir_access_token'),
});

Already have PLATFORM_API_URL / PLATFORM_API_KEY / FOIR_PREVIEW wired for the GraphQL path? createFoirClientFromEnv(env, { generated }) reuses them, including the preview sk_ key and the draft schema channel.

Typed GraphQL from a server loader

foirRequest is a typed fetch wrapper, not a GraphQL client: it carries the Foir-specific transport concerns (x-api-key, preview schema channel, error shape) and runs generated TypedDocumentNodes. Bring-your-own-client consumers (Apollo/urql/react-query) use the generated documents directly and replicate those headers.

import { foirRequest, type FoirEnv } from '@foir/sdk/client';
import { HomePageDocument } from './generated/graphql';

export async function loader({ context }: LoaderArgs) {
  const env: FoirEnv = {
    PLATFORM_API_URL: context.env.PLATFORM_API_URL,
    PLATFORM_API_KEY: context.env.PLATFORM_API_KEY,
  };
  const data = await foirRequest(env, HomePageDocument, { slug: 'home' });
  return data.homePage;
}

Customer auth

import {
  loginWithPassword,
  authedRequest,
  type SessionAdapter,
} from '@foir/sdk/auth';
import { MyProfileDocument } from './generated/graphql';

// SessionAdapter = { get, set, delete } over your session store (cookie, KV…).
const customer = await loginWithPassword(adapter, env, { email, password });

// Bearer-authed request with automatic refresh-and-retry on expired sessions.
const { myProfile } = await authedRequest(adapter, env, MyProfileDocument);

React

@foir/sdk/react builds the reactive layer on top of @foir/sdk/auth. Wrap your app once, then read the session with hooks or drop in the components. The default session store is localStorage; pass your own adapter for cookies.

'use client';
import { FoirProvider, SignIn, UserButton, useFoirSession } from '@foir/sdk/react';

const env = {
  PLATFORM_API_URL: process.env.NEXT_PUBLIC_PLATFORM_API_URL,
  PLATFORM_API_KEY: process.env.NEXT_PUBLIC_PLATFORM_API_KEY, // a publishable pk_ key
};

export function App() {
  return (
    <FoirProvider env={env}>
      <Header />
      <SignIn afterSignInUrl="/dashboard" />
    </FoirProvider>
  );
}

function Header() {
  const { isAuthenticated, isLoading } = useFoirSession();
  if (isLoading) return null;
  return isAuthenticated ? <UserButton afterSignOutUrl="/" /> : null;
}

For a custom form, use useFoirAuth() (signIn, register, signInWithOtp, requestOtp, signInWithProvider, signOut, getToken) and useFoirUser().

Backend-less SPA (Foir-hosted sessions)

By default <FoirProvider> stores tokens in localStorage. If your SPA has no backend but you have a verified custom login domain on the same registrable domain (app.acme.com + login.acme.com), prefer hostedSession: the durable credential becomes an HttpOnly cookie Foir sets on your domain, and the 15-minute access token lives only in memory. Nothing authentication-related is reachable from JS, so XSS has nothing to exfiltrate — the BFF security posture without a backend.

<FoirProvider
  env={{ PLATFORM_API_URL: 'https://api.foir.dev/graphql', PLATFORM_API_KEY: 'pk_…' }}
  hostedSession={{ loginOrigin: 'https://login.acme.com' }}
>
  <App />
</FoirProvider>

Sign-in is a redirect, not a form — useFoirAuth().hostedSignInUrl() gives you the URL (the password/OTP actions throw in this mode). getToken() exchanges the cookie and re-exchanges it silently before expiry; signOut() revokes the session server-side. Everything else — useFoirSession, useFoirUser, <UserButton> — behaves the same.

Framework-agnostic equivalent:

import { createFoirSessionClient } from '@foir/sdk/auth';

const session = createFoirSessionClient({ loginOrigin: 'https://login.acme.com' });
const token = await session.getToken(); // null when signed out

Next.js (App Router, BFF)

@foir/sdk/next keeps the tokens in HttpOnly cookies server-side. Protect routes with the middleware, read the session in a server component, and sign in from a route handler.

// middleware.ts
import { createFoirMiddleware } from '@foir/sdk/next';
export const middleware = createFoirMiddleware({ signInUrl: '/sign-in' });
export const config = { matcher: ['/dashboard/:path*'] };
// app/dashboard/page.tsx (Server Component)
import { cookies } from 'next/headers';
import { getFoirSession } from '@foir/sdk/next';

export default async function Dashboard() {
  const customer = await getFoirSession(await cookies());
  return <p>Signed in as {customer?.email}</p>;
}
// app/sign-in/route.ts (Route Handler)
import { cookies } from 'next/headers';
import { foirSignInWithPassword } from '@foir/sdk/next';

export async function POST(req: Request) {
  const { email, password } = await req.json();
  await foirSignInWithPassword({ env, store: await cookies(), email, password });
  return Response.json({ ok: true });
}

Codegen

// codegen.ts
import { foirCodegen } from '@foir/sdk/codegen';

export default foirCodegen({
  apiUrl: process.env.PLATFORM_API_URL!,
  apiKey: process.env.PLATFORM_API_KEY!,
  // documents: 'app/graphql/foir/**/*.graphql' (default)
  // outputDir: './app/gql/foir/' (default)
});

Editor extensions

import { EditorProvider, useEditor } from '@foir/sdk';

function App() {
  return (
    <EditorProvider>
      <MyEditor />
    </EditorProvider>
  );
}

function MyEditor() {
  const { init, client } = useEditor(); // record context + platform GraphQL client
  // ...
}

License

MIT