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

@framework-cwf/contracts

v0.9.0

Published

Zod schemas and TypeScript types for every value that crosses a boundary (website-config, postMessage protocol, webhook payloads, embed URL builder).

Readme

@framework-cwf/contracts

Zod schemas and inferred TypeScript types for every value that crosses a boundary between the Admin platform (booking-api / booking-web) and the customer-website framework. A change to a schema here is intended to fail CI on both sides simultaneously.

This package has no runtime code other than the schemas themselves. It is the first dependency of every other @framework-cwf/* package.

What's in here

| Schema | Purpose | | ------------------------------------------------------------- | ------------------------------------------------------------------------------ | | WebsiteConfigSchema | Top-level wrapper: { operational, marketing? } — the wire format. | | OperationalConfigSchema | Existing booking-api payload (business, locations, staff, etc.). | | MarketingConfigSchema | New admin-authored brand / copy / SEO content. | | BrandSchema, HeroSchema, ValuePropsSchema, … | Marketing sub-blocks. | | EMBED_VARIANTS, HEADING_FONTS, BODY_FONTS, ROUTE_KEYS | Const tuples — single source of truth for whitelists / route names. | | SitePublishedEventSchema | site.published webhook body emitted by booking-api to the deploy controller. | | signWebhookPayload, verifyWebhookSignature | HMAC-SHA256 helpers; verification uses crypto.timingSafeEqual. |

Inferred TypeScript types are exported alongside every schema (e.g. Brand for BrandSchema).

Schema layout

WebsiteConfigSchema
├── operational  (OperationalConfigSchema)
│   ├── business           — name, tagline, contact, est, logo refs
│   ├── booking            — theme + brand colour + header/show toggles
│   ├── locations[]        — guid (UUID), name, address, hours[], serviceCategories[]
│   ├── staff[]            — id, business_user_guid (UUID, optional), name, role
│   ├── hours[]?           — present when published at top level instead of per-location
│   └── serviceCategories[]? — same: optional top-level shape
└── marketing? (MarketingConfigSchema)
    ├── brand              — embedVariant, hex colours, fonts (whitelist), cornerRadius, density
    ├── hero               — headline, subheadline, primaryCta, optional secondaryCta + image
    ├── valueProps         — exactly 3 cards
    ├── about              — headline + body
    ├── gallery[]          — imageRef + altText (required, ≥4 chars)
    ├── testimonials[]     — text + attribution + optional location/rating/imageRef
    ├── faqs               — pages: Record<RouteKey, FaqEntry[]>
    ├── seo                — canonicalHostname, defaults, robots (prod / non-prod), per-page overrides
    └── pages              — enabled: Record<RouteKey, boolean>

RouteKey is 'home' | 'services' | 'about' | 'visit'.

Validation rules enforced

  • All hex colour fields match /^#[0-9a-fA-F]{6}$/.
  • All font fields must be members of HEADING_FONTS / BODY_FONTS.
  • All image refs are non-empty strings.
  • Gallery altText is required and at least 4 chars.
  • seo.canonicalHostname must be a valid URL.
  • Robots directives must match the (no)?index,(no)?follow shape.
  • location.guid must be a valid UUID.
  • staff.business_user_guid must be a valid UUID when present (the field is optional — see "Notes on shape divergence" below).

What's new in 0.2.0

Seven additive, fully-optional fields. Every 0.1.x payload still parses without modification — back-compat is non-negotiable.

| Field | Where | Why | | ----------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | business.businessType | OperationalConfig.business | schema.org LocalBusiness sub-type. Enum: hair_salon | barbershop | beauty_salon | spa | local_business. Pilot defaults to hair_salon when unset. | | services[].priceCurrency | OperationalConfig.services[] | ISO 4217 currency. Whitelist: GBP | EUR | USD | AUD | CAD. Consumers fall back to GBP when unset. | | business.addressStructured | OperationalConfig.business | schema.org PostalAddress{ streetAddress, addressLocality, addressRegion, postalCode, addressCountry, latitude?, longitude? }. Coexists with the freeform address string. | | locations[].hoursStructured | OperationalConfig.locations[] | schema.org OpeningHoursSpecification{ dayOfWeek, opens, closes, closed? }[] with HH:mm times. Coexists with the freeform hours[] array. | | testimonials[].verified | MarketingConfig.testimonials[] | Trust-badge flag. Optional — consumers treat undefined as false. | | seo.pages[route].noindex | MarketingConfig.seo.pages | Boolean noindex alongside the legacy robots directive string. When both are set, noindex: true wins (resolution in @framework-cwf/seo). | | seo.hreflang | MarketingConfig.seo | Per-locale alternates — { lang: BCP-47, href: URL }[]. Drives <link rel="alternate" hreflang> and Next.js Metadata.alternates.languages. |

New whitelist exports: BUSINESS_TYPES, PRICE_CURRENCIES, DAYS_OF_WEEK. New schemas: BusinessTypeSchema, PriceCurrencySchema, PostalAddressSchema, HoursStructuredEntrySchema, HreflangEntrySchema, DayOfWeekSchema. New inferred types: BusinessType, PriceCurrency, PostalAddress, HoursStructuredEntry, HreflangEntry, DayOfWeek.

Notes on shape divergence (operational config)

The two reference operational payloads — sniply-barber-fe/config.js and sniply-barber-fe/uploads/config.json — disagree on three things, and the schema is permissive across all three so both parse without modification:

  1. hours placement. Sometimes inside each location, sometimes at the top level of operational. Both are accepted; both fields are optional.
  2. serviceCategories placement. Same pattern — per-location or top-level.
  3. Staff identifiers. Some payloads carry UUIDs in both id and business_user_guid; others carry slugs ("marcus") in id only. So:
    • staff.id is z.string() (any opaque identifier).
    • staff.business_user_guid is z.string().uuid().optional().

British/US spelling in booking (primaryColour vs bg_color) is preserved deliberately. Renaming would require a coordinated migration across booking-api and every published config.

All operational schemas use .passthrough() so unknown fields (e.g. staff.tier, staff.priceMod) are preserved through round-trips rather than silently dropped.

Worked example — a complete website-config payload

import {
  WebsiteConfigSchema,
  type WebsiteConfig,
} from "@framework-cwf/contracts";

const payload: WebsiteConfig = {
  operational: {
    business: {
      name: "SNIP-LY Barber",
      tagline: "Sharp cuts. Clean lines.",
      description: "London's modern barbershop brand.",
      phone: "+44 20 7946 0100",
      email: "[email protected]",
      address: "31 South Molton Street, London, W1K 5RN",
      logo: null,
      icon_logo: null,
    },
    booking: {
      theme: "dark",
      primaryColour: "#ff0000",
      bg_color: "#1a1a1a",
    },
    locations: [
      {
        guid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        name: "Mayfair",
        address: null,
        phone: null,
        email: null,
        image: null,
        hours: [
          { day: "Mon", hours: "9 — 17" },
          { day: "Sat", hours: "closed", closed: true },
        ],
        serviceCategories: [
          {
            id: "11",
            name: "Classic Cuts",
            description: "Scissor and clipper cuts",
            icon: null,
            priceImage: null,
            services: [
              { name: "Classic Cut", duration: "30 min" },
              { name: "Skin Fade", duration: "45 min" },
            ],
          },
        ],
      },
    ],
    staff: [
      {
        id: "c1d20001-0000-4000-8000-000000000001",
        business_user_guid: "c1d20001-0000-4000-8000-000000000001",
        name: "James Carter",
        role: "OWNER",
        image: null,
      },
    ],
  },
  marketing: {
    brand: {
      embedVariant: "sharp",
      primaryColor: "#ff0000",
      bgColor: "#1a1a1a",
      accentColor: "#00aa55",
      headingFont: "Anton",
      bodyFont: "Inter",
      cornerRadius: 4,
      density: "comfortable",
    },
    hero: {
      headline: "Sharp cuts. Clean lines.",
      subheadline: "Mayfair and Shoreditch. Walk-ins welcome.",
      heroImageRef: "hero/main.jpg",
      primaryCta: { label: "Book now", href: "/book" },
    },
    valueProps: [
      {
        iconRef: "icons/scissors",
        title: "Craft",
        body: "Trained cutters only.",
      },
      {
        iconRef: "icons/clock",
        title: "On time",
        body: "Never wait past 5min.",
      },
      { iconRef: "icons/star", title: "4.9★", body: "From 1,200+ reviews." },
    ],
    about: { headline: "About SNIP-LY", body: "London's modern barbershop." },
    gallery: [{ imageRef: "gallery/01.jpg", altText: "Skin fade detail shot" }],
    testimonials: [
      { text: "Best fade in London.", attribution: "Tom R.", rating: 5 },
    ],
    faqs: {
      pages: {
        home: [
          {
            question: "Do you take walk-ins?",
            answer: "Yes — but booking guarantees a slot.",
          },
        ],
      },
    },
    seo: {
      canonicalHostname: "https://snip-ly.co.uk",
      defaults: {
        titleTemplate: "%s | SNIP-LY Barber",
        descriptionTemplate: "London barbers — %s.",
        ogImageRef: "og/default.jpg",
      },
      robots: {
        production: "index,follow",
        nonProduction: "noindex,nofollow",
      },
      pages: {
        "/": { title: "SNIP-LY Barber — London cuts and shaves" },
      },
    },
    pages: {
      enabled: { home: true, services: true, about: true, visit: true },
    },
  },
};

const parsed = WebsiteConfigSchema.parse(payload); // throws on validation failure

site.published webhook

booking-api emits a site.published webhook to the deploy controller after every successful publish. The body matches SitePublishedEventSchema; two headers (X-Sniply-Signature, X-Sniply-Timestamp) carry the HMAC-SHA256 signature over ${timestamp}.${rawBody}. See the JSDoc at the top of src/webhook.ts for the full response contract (202 / 401 / 404 / 500), the 1-hour exponential-backoff retry policy, and the version_id-as-idempotency-key convention.

import {
  WEBHOOK_SIGNATURE_HEADER,
  WEBHOOK_TIMESTAMP_HEADER,
  signWebhookPayload,
  verifyWebhookSignature,
} from "@framework-cwf/contracts";

// Sender (booking-api)
const payload = JSON.stringify(event);
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = signWebhookPayload({ payload, timestamp, secret });
await fetch(url, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    [WEBHOOK_SIGNATURE_HEADER]: signature,
    [WEBHOOK_TIMESTAMP_HEADER]: timestamp,
  },
  body: payload,
});

// Receiver (deploy controller)
const result = verifyWebhookSignature({
  payload: rawBody,
  signature: req.headers[WEBHOOK_SIGNATURE_HEADER.toLowerCase()],
  timestamp: req.headers[WEBHOOK_TIMESTAMP_HEADER.toLowerCase()],
  secret,
});
if (!result.valid) return res.status(401).json({ error: "invalid_signature" });

Commands

pnpm --filter @framework-cwf/contracts typecheck
pnpm --filter @framework-cwf/contracts test