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

@fun-xyz/fiat-contract

v0.4.0

Published

Published conformance contract for Fun's headless fiat onramp: FlowState/Transition types, zod schemas, the transition table as data, recorded fixtures, assertion helpers.

Readme

fiat-contract

The published contract between fun-backend (emits envelopes) and funkit/connect-core (renders them). Both repos test against it; neither owns it.

Four things, zero runtime logic beyond validation:

| File | What it is | | --- | --- | | src/types.ts | FlowState · Transition · FailureReason · Surface · Instructions · FormDescriptor · OrderStatus · StepResponse | | src/schemas.ts | zod mirrors of every type — the single runtime validator | | src/table.ts | the transition table as data: per state, its legal transition set, the states any call from it may return, and terminal: boolean | | src/assert.ts + src/fixtures/ | assertEnvelope · assertLegalEmission · assertLegalReturn · walkTable · fixture loader + 16 recorded envelopes (inlined as data — no filesystem, so React Native can bundle it) |

Three entry points — production vs test-time

| Import | Weight | Contains | Used by | | --- | --- | --- | --- | | @fun-xyz/fiat-contract/types | 0.1 KB (types erase) | every type; no runtime values | production, both repos | | @fun-xyz/fiat-contract/table | 13.8 KB, zero deps | TRANSITION_TABLE, stateKey, tableEntry, isTerminal, walkTable, TABLE_VERSION, TERMINAL_ORDER_STATUSES, DOCUMENTED_ENDPOINTS | production frontend + backend | | @fun-xyz/fiat-contract | 47.6 KB, needs zod | the above + 46 zod schemas + assertions + 16 fixtures | tests, and backend dev/test guards |

./table is not a micro-optimisation. Terminality is table data a shipped client must read (litmus rule 3 — clients never infer it), and Metro has no cross-module tree-shaking on by default, so importing isTerminal from the root would ship zod and all 16 fixtures into a React Native bundle. Two CI jobs hold that line: consumer asserts requiring ./table never loads zod into the process, and metro bundles ./table with real Metro — in both package-exports modes, since Metro before RN 0.79 ignores exports entirely — then greps the emitted bundle for zod, fixtures and schemas and executes it.

Source of truth: Fiat Client Contract (§The envelope · §Conformance package · §split InputSpec — ACCEPTED) and Fiat Frontend — State Machine & Screen Map (✅ Decisions · per-screen State details · Event bindings per flow state). Tracking: Headless Fiat Onramp · this package is ENG-5268.

The litmus rules

  1. Errors are fields, never states. Every fallible state carries error?: FailureReason. Stay-on-screen ⇒ an error field. Change-screen ⇒ a different state (session expiry just returns a SESSION_AUTH envelope — there is no error routing table).
  2. params = server literals · inputs = collected specs · expects = injected surface results. body = {…params, …collected(inputs), …injected(expects)}; a key collision across the three is a contract violation, not last-write-wins. zod rejects a FieldSpec hiding in params, so the old double-duty bug is unrepresentable.
  3. Terminality comes from the table. terminal is enumerated per state entry — never inferred from transitions.length. KYC ON_HOLD carries transitions: [] and is not terminal. ORDER{CREATED} terminality rides status ∈ TERMINAL_ORDER_STATUSES, and a terminal status may still carry a recovery CTA (FAILED + retryable failureReason).
  4. SUBMIT is a user-fired request, not a POST. The harness parses the verb off the endpoint string; GET /fiat/orders/:id under SUBMIT is legal.
  5. Fixtures stay synthetic. q_8f2, o_31c, eyJ…, "…" — never paste a real session token, bank field, or PII value into a fixture. Redaction applies to fixtures too.

Versioning — the package version IS the table version

TABLE_VERSION (exported from src/table.ts) must equal package.json#version; a test asserts it. That version rides the capability handshake (supportedProviders / supportedSurfaces / supportedStepKinds + table version), which is how BE↔FE skew stays explicit and designed-for: the server never routes a flow into a state kind the installed SDK didn't declare.

Starting at 0.1.0. Bump the minor for additive vocabulary (a new field, a new fixture); bump the major when an existing shape changes meaning.

How each repo consumes it

fun-backend — adapters compile their provider's real flow into this state machine.

import { assertEnvelope, assertLegalEmission, walkTable } from '@fun-xyz/fiat-contract';

assertLegalEmission(state, transitions);              // per emission: adapter conformance
assertEnvelope(outgoing);                             // outgoing-envelope validation in dev/test
const owed = walkTable((entry) => entry.allowedTransitions); // what the adapter must emit

Its own suites: adapter conformance (fixture-driven + property-generated states), outgoing-envelope validation, and the scheduled provider-sandbox drift run diffed against src/fixtures.

connect-core — the harness obeys transitions for sequencing and owns rendering.

import { FIXTURES, loadFixture, walkTable } from '@fun-xyz/fiat-contract';

walkTable((entry) => expect(computePage(entry.key)).toBeDefined()); // exhaustive resolver walk
FIXTURES.forEach(({ id }) => renderCold(loadFixture(id)));         // stale-rule survival

Its own suites: exhaustive computePage table-walk, fixture-driven cold-render tests, harness units (any-state handling, same-state error re-entry retains form values, one-live-envelope focus gating, idempotency-key reuse).

Usage

Everything below is compiled against the packed tarball in CI (scripts/check-readme-examples.sh), so these examples cannot drift from the API.

Production: a backend route handler

The backend builds envelopes, so its production use is entirely compile-time — the types are the verification. Runtime assertions stay behind a dev/test guard.

import type { FlowState, StepResponse, Transition } from '@fun-xyz/fiat-contract/types';

export async function createOrder(quoteRef: string): Promise<StepResponse> {
  const state: FlowState = {
    kind: 'PAYMENT',
    phase: 'INSTRUCT',
    instructions: { kind: 'BANK_FIELDS', fields: await beneficiaryFields(), expiresAt },
  };

  const transitions: Transition[] = [
    { id: 'confirm_paid', mode: 'SUBMIT', endpoint: `POST /fiat/orders/${orderId}/confirm-payment` },
    // endpoint is a bounded union: `POST /fiat/orders/${string}/confirm-paymnt` is a compile error,
    // and so is any path outside the documented /fiat/* surface
  ];

  if (!isProduction) {
    const { assertLegalEmission } = await import('@fun-xyz/fiat-contract');
    assertLegalEmission(state, transitions); // dev/test only — never on the hot path
  }

  return { state, provider: 'TRANSAK', transitions };
}

What the types buy at compile time: a state kind that isn't in the union won't type, a transition mode/shape mismatch won't type, and — since FiatEndpoint is a bounded union — neither will a typo'd endpoint. No runtime cost.

Production: a frontend screen

The client narrows on the state and reads terminality from the table. Both imports are production-safe: types erase, ./table is 13.8 KB with no zod.

import { isTerminal, stateKey } from '@fun-xyz/fiat-contract/table';
import type { StepResponse } from '@fun-xyz/fiat-contract/types';

export function FiatScreen({ env }: { env: StepResponse }) {
  const page = computePage(stateKey(env.state), clientLocal); // client owns state → screen
  const done = isTerminal(env.state);                          // table data, never inferred

  if (env.state.kind === 'PAYMENT' && env.state.phase === 'INSTRUCT') {
    return render(page, env.state.instructions); // narrowed: instructions exists here
  }
  if (env.state.kind === 'ORDER' && env.state.phase === 'CREATED') {
    return render(page, { status: env.state.status, done });
  }
  return render(page, env.state);
}

isTerminal is the whole reason ./table exists as a runtime entry: KYC ON_HOLD carries transitions: [] and is not terminal, while ORDER{CREATED, SETTLED} is — no client may re-derive that rule locally.

Production: the transitions loop

import type { StepResponse, Transition } from '@fun-xyz/fiat-contract/types';

function useTransitions(env: StepResponse) {
  return {
    ctas: env.transitions.filter((t) => t.mode === 'SUBMIT'),          // render buttons
    poll: env.transitions.find((t) => t.mode === 'AWAIT'),             // harness schedules
    surface: env.transitions.find((t) => t.mode === 'CLIENT_SURFACE'), // harness mounts
  };
}

function bodyFor(t: Transition, collected: Record<string, unknown>, surface?: Record<string, unknown>) {
  if (t.mode === 'SUBMIT') return { ...t.params, ...collected };  // verb parsed from t.endpoint
  if (t.mode === 'CLIENT_SURFACE') {
    const injected = Object.fromEntries((t.report.expects ?? []).map((k) => [k, surface?.[k]]));
    return { ...t.report.params, ...injected };
  }
  return { ...t.poll.params };
}

Screens never inspect the array themselves; they receive ctas and bind labels (label(t.id) ?? t.labelFallback — the contract carries no display copy). A key colliding across params / inputs / expects is a contract violation, not last-write-wins.

Testing against it

These import from the root, which carries zod. Test-time only.

Validate an envelope at the boundary

assertEnvelope parses and returns a typed envelope, or throws ContractViolation listing every problem. Use it on the way out of fun-backend (dev/test) and on the way in to connect-core tests.

import { assertEnvelope, ContractViolation } from '@fun-xyz/fiat-contract';

try {
  const envelope = assertEnvelope(await res.json());
  //    ^? StepResponse — state is a narrowable discriminated union from here on
  if (envelope.state.kind === 'PAYMENT' && envelope.state.phase === 'INSTRUCT') {
    render(envelope.state.instructions); // narrowed: instructions exists, quote does not
  }
} catch (err) {
  if (err instanceof ContractViolation) console.error(err.issues); // ['state.quote: Required', …]
  throw err;
}

Assert an emission is legal (backend adapter conformance)

Shape validity is not sequence validity. assertLegalEmission judges (state, transitions) against the table: is each transition in this state's legal set, does a terminal state carry none, does a CLIENT_SURFACE transition have a Surface in state, do params/inputs/expects collide.

import { assertLegalEmission, checkLegalEmission } from '@fun-xyz/fiat-contract';

// Throwing form — use in adapter unit tests
assertLegalEmission(
  { kind: 'SESSION_AUTH', channel: 'EMAIL_OTP' },
  [{
    id: 'verify',
    mode: 'SUBMIT',
    endpoint: 'POST /fiat/session/verify',
    params: { quoteRef: 'q_8f2' },              // server literal, spread verbatim
    inputs: { code: { type: 'TEXT', length: 6 } }, // spec the client collects
  }],
);

// Non-throwing form — use when you want to report in bulk across many generated states
const { issues, unjudgeable } = checkLegalEmission(state, transitions);
if (!unjudgeable && issues.length) report(issues);

unjudgeable is true only for states the docs publish no transition set for (FUN_AUTH) — it means "cannot judge", never "passed".

Walk the table exhaustively (frontend resolver coverage)

The table is data, so a runtime walk proves every state resolves to a screen — stronger than TS exhaustiveness alone, because it also fails when a new state is added to the contract.

import { walkTable, isTerminal, TRANSITION_TABLE } from '@fun-xyz/fiat-contract';

it('every contract state resolves to a screen', () => {
  walkTable((entry) => {
    expect(computePage(entry.key)).toBeDefined();
  });
});

// Terminality is table data — never `transitions.length === 0`
isTerminal({ kind: 'KYC', phase: 'NO_ACTION_REQUIRED', reason: 'ON_HOLD' }); // false: empty, not over
isTerminal({ kind: 'ORDER', phase: 'CREATED', status: 'SETTLED' });          // true
isTerminal({ kind: 'ORDER', phase: 'CREATED', status: 'PROCESSING' });       // false

TRANSITION_TABLE['PAYMENT/INSTRUCT'].mayReturn; // ['ORDER/CREATED']

Render every fixture cold (stale-rule survival)

Any call may return any state, so every screen must render from a cold envelope with no prior context. The fixtures are the FE doc's own envelopes, so this is a test against the spec.

import { FIXTURES, loadFixture, assertFixture } from '@fun-xyz/fiat-contract';

FIXTURES.forEach(({ id, stateKey, docRef }) => {
  it(`${id} renders cold (${docRef})`, () => {
    const { envelope } = assertFixture(id); // validated + emission-legality checked
    expect(() => renderCold(envelope)).not.toThrow();
    expect(computePage(stateKey)).toBeDefined();
  });
});

loadFixture('screen-10-kyc-on-hold'); // raw JSON, fresh deep copy, `unknown` — validate before use

Use a schema directly

All 46 schemas are exported when you need to validate a fragment rather than a whole envelope. They are typed z.ZodType<T>, so you get .parse / .safeParse / .optional() — not .shape or .extend, deliberately.

import { FlowStateSchema, QuoteSchema, TransitionSchema } from '@fun-xyz/fiat-contract';

const quote = QuoteSchema.parse(row.quote_json);
const result = FlowStateSchema.safeParse(input);
if (!result.success) log(result.error.issues);
void TransitionSchema;

Check the handshake version

import { TABLE_VERSION } from '@fun-xyz/fiat-contract';

const handshake = {
  supportedProviders: ['TRANSAK'],
  supportedSurfaces: ['PCI_COMPONENT', 'PAY_SHEET'],
  supportedStepKinds: ['QUOTE', 'SESSION_AUTH', 'KYC', 'PAYMENT', 'ORDER', 'BLOCKED'],
  tableVersion: TABLE_VERSION, // the package version IS the table version
};

Install

pnpm add -D @fun-xyz/fiat-contract
import type { StepResponse } from '@fun-xyz/fiat-contract/types'; // production, 0 KB
import { isTerminal } from '@fun-xyz/fiat-contract/table';         // production, 13.8 KB, no zod
import { assertEnvelope } from '@fun-xyz/fiat-contract';           // tests only, pulls zod

Public npm — no .npmrc, no token, no registry config. Releases fire from a v* tag (.github/workflows/release.yml), which refuses to publish when the tag and package.json version disagree, and no-ops when that version is already published.

Sourcemaps are excluded from the tarball ("!dist/**/*.map" in files) — unlike the funkit SDK packages, which publish theirs on purpose. These declarations carry open-decision commentary that has no reason to reach consumers. 50.5 kB, 21 files.

How connect-core consumes this

connect-core bundles ./table and ./types into its own dist rather than shipping them as resolvable dependencies. One line of build config, not a copy of the code: one authored table, pinned in connect-core's lockfile, recompiled into its output every build. Copy-pasting the table into connect-core/src is not this, and would defeat the package.

// funkit/packages/connect-core/build.config.js — inside the externalize plugin
const BUNDLED_SPECIFIERS = new Set([
  '@fun-xyz/fiat-contract/table',
  '@fun-xyz/fiat-contract/types',
]);
build.onResolve({ filter }, (args) =>
  BUNDLED_SPECIFIERS.has(args.path)
    ? undefined                                  // let esbuild bundle it in
    : { external: true, path: args.path },       // everything else stays external
);

Why bundle rather than depend:

  • Pins the tested pair. As a dependency, a partner's install resolves the semver range at their install time, so an SDK tested against table 0.2.1 could run against 0.3.0 in the field. Bundling compiles TABLE_VERSION into the release, which is what the handshake then reports.
  • Metro never sees the subpath, so the package-exports question (§Packaging) cannot reach a partner's bundler.

Consequences:

  • The build exception needs its comment, or it gets deleted as dead config. A test in connect-core asserts the table inlines and that zod and the fixtures stay out.
  • connect / connect-rn should import the table from connect-core, not bundle their own copy (~14 KB apiece).
  • The shipped table version is not readable from a partner's node_modules — read the handshake.

The root entry stays a plain devDependency in both repos: test-only, so it never reaches a published artifact.

Packaging

Dual CJS + ESM, same shape as @funkit/fun-relay, because the two consumers load it differently:

| Requirement | Source | How it's met | | --- | --- | --- | | require() must work | fun-backend/apps/api-server compiles module: commonjs and runs plain node — no bundler | esbuild emits dist/index.js (CJS) + dist/index.mjs (ESM); exports maps require/import; no "type": "module" | | Zero Node builtins | connect-core is React Native — Metro cannot resolve node:fs | fixtures are inlined as generated TS data (src/fixtures/data.ts), so nothing touches the filesystem; platform: browser, every bare import external | | Subpath imports must resolve without exports support | Metro only reads package.json#exports from RN 0.79 on, and @funkit/connect-rn accepts react-native: >=0.74 | root compat stubs table.js / types.js (+ .d.ts) that re-export dist/. Without them Metro fails with "Unable to resolve module" — verified, not theorised. Resolvers that do read exports never see the stubs | | Production code must not pull zod or fixtures | connect-core ships to React Native, where Metro does not tree-shake unused exports by default | three entry points: ./types (erased), ./table (13.8 KB, zero deps), root (schemas + fixtures, test-time). CI asserts require('.../table') never loads zod | | .d.ts must not lock a zod major | Both repos run skipLibCheck: true, which turns a broken declaration into a silent any | every exported schema is annotated z.ZodType<T>, so declarations name only z.ZodType; the structural drift checks stay module-private |

Declarations come from tsc --emitDeclarationOnly; esbuild only emits JS. The fixture .json files remain the verbatim record — data.ts is generated from them and CI fails if it drifts.

Scripts

npm run typecheck            # tsc --noEmit, strict; includes the schema↔type mirror assertions
npm test                     # vitest: fixtures, zod round-trip, table integrity, negative asserts
npm run generate:fixtures    # regenerate src/fixtures/data.ts from the .json record
npm run build                # tsc declarations + esbuild dual CJS/ESM into dist/
npm run check                # typecheck + test

./scripts/verify-consumer.sh 3.23.8 pnpm   # pack, install, require/import, and typecheck a
                                           # consumer with skipLibCheck OFF — catches packaging
                                           # faults the in-repo suite structurally cannot
./scripts/check-readme-examples.sh         # compile every ```ts block in this README against
                                           # the packed package
./scripts/verify-metro.sh                  # bundle ./table with real Metro in both
                                           # package-exports modes; assert no zod/fixtures reach
                                           # the bundle, and that it executes

Dependencies

zod is an optional peer, range ^3.22.0 || ^4.0.0. Only the root entry needs it; ./table and ./types have no runtime dependency on zod, so an install that never imports the root entry needs no zod at all.

| Repo | zod | Notes | | --- | --- | --- | | funkit (pnpm 9.4) | 3.23.8 (apps/fits, apps/frog) | connect-core declares it explicitly — required, see below | | fun-backend (pnpm 10.16) | 3.25.76 | forced tree-wide by pnpm.overrides |

Neither repo is on zod 4; the || ^4.0.0 arm is verified, not required. CI runs the suite and a packed-tarball consumer check on 3.22.4, 3.23.8, 3.25.76 and 4.4.3. (zod 3.25.0 exactly is unusable — that release ships no dist/; 3.25.1+ is fine.)

A consumer importing the root entry must declare zod itself. The peer being optional means pnpm installs none by default, so a test/** import of the root entry fails with a missing module — loud, and the reason the peer is optional. When it was a required peer, pnpm instead resolved zod 4 and, under dedupe-peer-dependents, moved funkit's viem onto it, which abitype's ^3 >=3.22.0 peer rejects.

Declarations never bake in a zod major: every exported schema is annotated z.ZodType<T>. Consumers get .parse / .safeParse / .optional(), not .shape / .extend — a contract you validate against, not compose from.

Open decisions — encoded, not resolved

Every one of these is marked // TODO(open-decision): <doc ref> at the site that encodes it. The current documented shape is what ships; none of these are settled here.

| Open item | How 0.1.0 encodes it | | --- | --- | | orderId placement | Both: optional on PAYMENT{INSTRUCT} (FE doc v0) and optional as an envelope sibling (contract worked example). One fixture of each. | | PENDING_ORDER removal | Kind ships, with the removal proposal flagged on the type, the table entry, and the fixture. Screen 12 stays frozen. | | [OQ7] failure enumeration | The published FailureReason taxonomy only. Expired instructions, partial payment, per-rail cancel eligibility, and terminal-vs-escalating rejections are flagged unenumerated. | | Screen 11 escalation trigger | Encoded as published (SUBMIT GET /fiat/kyc + params: {tier}) with the doc's own warning that a GET carrying params is not a real shape. | | Card capture report target | Both topologies are legal in the table (POST /fiat/orders for capture-then-order; POST /fiat/orders/:id/surface-result otherwise) pending the Transak answer. | | Cancel placement | cancel is legal on PAYMENT{INSTRUCT} and ORDER{CREATED}, marked conditional on the placement decision. | | FUN_AUTH shape | challenge: Record<string, JsonValue>; the table entry is docStatus: 'UNSPECIFIED', so assertLegalEmission reports it unjudgeable instead of guessing. | | statusHistory element shape | The documented minimum ({status}) — no invented timestamps. | | QR_IMAGE instruction | In the union per §The envelope, flagged against OQ1's "deliberately not pre-declared". |

Two fixture gaps are declared in FIXTURE_COVERAGE_GAPS rather than filled with invented envelopes: FUN_AUTH (shape owned by the auth spike) and KYC/CAPTURE (Screen 8 tombstone, dropped from v1). Every other state has a recorded envelope.

Not decided here — needs a human

  • Whether PENDING_ORDER ships in the types or waits for the removal decision. It ships today (flagged everywhere) so the current server shape validates.
  • Repo hosting / org placement. The docs recommend a standalone repo (option B) or fun-backend (option C) — explicitly not funkit. Backend sign-off is pending; this repo is standalone, which is reversible either way.