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

@vibedout/api-contract

v0.2.1

Published

What a feature contributes to the public APIs without importing the feature that owns them: for servers (`/api/v1`, secret keys and scopes) the shape of an operation (method, path, scope, Zod schemas for the query, the path, the body and the answer, and a

Readme

@vibedout/api-contract

What a feature adds to the two public APIs of a vibedout app without importing the feature that owns them:

  • /api/v1, for servers (a secret vsk_ key with scopes), owned by the connectors feature: defineApi, slot worker-api.
  • /api/store, for a storefront on another origin (a publishable pk_ key with allowed origins, or a server's secret sk_ key that a browser never gets served; no scopes, a guest buyer), owned by the store feature: defineStoreApi, slot worker-store-api.

Hono and Zod are peer dependencies (Zod at run time only in openapi, Hono at run time only in testing); no runtime dependency.

The owners are the router, the key check, the rate limit (connectors for /api/v1; store for /api/store, by address and by key, with Retry-After), the OpenAPI 3.1 document and the usage counts of keys. A Worker block may not import another block, and a feature must work when the owner is not installed, so a feature that wants its own reads and writes in an API describes them here and lets the generated Worker barrel hand them over.

// src/worker/blocks/catalog/api.ts
import { defineApi, operationFor } from "@vibedout/api-contract";
import { z } from "zod";
import type { AppEnv } from "../../env";

const listProducts = operationFor<AppEnv>()({
  id: "listProducts",
  method: "get",
  path: "/products",                      // under /api/v1; /products/{id} for a path parameter
  scope: "catalog:read",                  // one of the scopes below
  summary: "The published products, newest first",
  query: z.object({ limit: z.coerce.number().int().min(1).max(100).default(24) }),
  response: z.object({ data: z.array(z.object({ id: z.string() })) }),
  async handler({ c, caller, query }) {   // the context, who is calling (never the key's secret), and the input, already checked
    return { data: await readProducts(c.env.DB, query.limit) };
  },
});

export const catalogApi = defineApi<AppEnv>({
  id: "catalog",
  scopes: [{ id: "catalog:read", description: { en: "Read the published products", "es-MX": "Leer los productos publicados" } }],
  operations: [listProducts],
});
// block.json
"packages": [{ "name": "@vibedout/api-contract", "range": "^0.2.0" }],
"wiring": { "worker-api": [{ "file": "src/worker/blocks/catalog/api.ts", "export": "catalogApi", "scopes": ["catalog:read"] }] }

The engine writes registerApi(catalogApi) into src/worker/blocks.gen.ts and refuses two features that declare one scope. connectors reads the registry inside a request (apiContributions()): it authenticates the key, finds the operation, checks the scope, validates the query, the path and the body with the feature's schemas, calls the handler, and writes the operation into its OpenAPI document from the same schemas.

The storefront API (defineStoreApi)

// src/worker/blocks/checkout/store-api.ts
import { defineStoreApi, storeOperationFor } from "@vibedout/api-contract";

const op = storeOperationFor<AppEnv>();
const getOrder = op({
  id: "getOrder", method: "get", path: "/orders/{orderId}", summary: "One order",
  proof: { id: "order", param: "orderId" },        // the buyer must show the order's token (header x-order-token) FOR THIS orderId
  params: z.object({ orderId: z.string() }),
  response: orderView,                             // a Zod schema with an .meta({ id }) is a named component of the document and a type of the SDK
  async handler({ c, params, proof }) { /* ... */ },
});

export const checkoutStoreApi = defineStoreApi<AppEnv>({
  id: "checkout",
  proofs: [{ id: "order", header: "x-order-token", description: { en: "...", "es-MX": "..." }, verify: ({ token, subject }) => /* the subject proved, or undefined */ }],
  operations: [getOrder],
});
// block.json
"wiring": { "worker-store-api": [{ "file": "src/worker/blocks/checkout/store-api.ts", "export": "checkoutStoreApi" }] }
  • No scopes. The publishable key is the whole permission, so a storefront operation can read what a shop shows anyone and write what a guest may write (a cart, a checkout). Anything more is a server operation.
  • A proof is a header the buyer holds for a thing of theirs (a cart, an order). The feature that makes the thing declares the proof (proofs, with verify) and the operation asks for it by id; another feature may ask for a proof it did not declare (the fulfilment tracking asks for order). An operation that asks for a proof no installed feature declares is left out of the router and the document. verify answers the subject it proves or undefined, never throws, and does not look at the database to say no, so "no such order" and "not your order" read the same.
  • Who is asking (caller): keyId, keyTitle, channelId, channelHandle, kind (publishable or secret) and allowsUrl(url), which says whether an address is at an origin the key lists (the owner's one rule for an origin). A contribution that sends a buyer somewhere (a payment's return page) asks allowsUrl and never reads the key's origins itself.
  • The owner's order of checks (the store feature's api.ts): the rate limit by address, the key and the origin, the operation (404), the proof (401 <id>_token_missing / <id>_token_invalid), the path, query, headers and body against the operation's schemas (400), the handler. Each answer is no-store unless the handler says otherwise.
  • registerStoreApi refuses (a Worker that would answer half an API does not start) an invalid contribution, a duplicate id and two operations a request could mean both of; a feature's own test calls checkStoreContribution(checkoutStoreApi) and expects [].

Refusals (errors)

An operation lists the refusals it makes of its own, so the document says them and a typed client can branch on them (50.17-T52):

const buyNow = op({
  id: "buyNow", /* ... */
  errors: [
    { status: 403, code: "forbidden", reason: "guest_checkout_off", description: "The shop takes no orders from a storefront right now." },
    { status: 409, code: "conflict", reason: "sold_out", description: "That is sold out." },
    { status: 404, code: "not_found", description: "There is no such thing to buy." },     // a refusal with no `reason` is fine
  ],
  handler(...) { throw new AppError(409, "conflict", "...", { reason: "sold_out" }); },
});

status goes with its envelope code (ERROR_STATUS_CODES: 400 bad_request or validation_failed, 401, 403, 404, 409, 429, 500 and 502 internal_error), a reason is lowercase words with _ (the word in details.reason), a refusal is listed once, and checkContribution / checkStoreContribution say what is wrong. operationObject writes one response for each status: the error envelope as its body, the refusals in words and, for a program, in x-vibedout-errors ([{ code, reason?, description }]); the owner's own answers (the input's 400, a path's 404, the key's 401/403/429) are written the same way with errorResponses(errors, into), which adds to a response the owner already wrote and says a refusal it already lists in the operation's own words. The handler is not checked against its list (like response); a test of the operation makes each refusal happen. The generated client (packages/store-sdk) has refusalOf(operationId, error) that returns the documented refusal.

The documents

openapi writes OpenAPI 3.1 pieces from the same Zod schemas the handlers validate with, so a document cannot promise what the code does not check: SchemaComponents collects schemas with an id as named components (#/components/schemas/Order), schemaOf writes one (JSON Schema 2020-12; Zod 4.4 and 4.5 differ in how they write a root id, both are handled), operationObject writes an operation (parameters from the path, query and header schemas, the body, the responses, the security), normalizeSchema tidies what a client generator trips on (additionalProperties: false, the safe-integer bounds Zod adds to every integer, a pattern that repeats a format, and anyOf [T, null] written as type: [T, "null"]), and apiErrorSchema is the error envelope both APIs use. Both owners use these helpers, so the two documents have one dialect; packages/store-sdk in the vibedout repository holds the committed reference documents of both APIs and a typed TypeScript client generated from them.

Testing an operation

import { exerciseOperation } from "@vibedout/api-contract/testing";

const { status, answer, problems } = await exerciseOperation(checkoutStoreApi, "getOrder", { env, params: { orderId }, proof: { id: "order", subject: orderId } });
expect(problems).toEqual([]);            // the answer is what the operation's `response` schema (and so the document) says, field for field

It runs the handler inside a real Hono request, checks the test's input with the operation's own schemas and compares the answer with the response schema; it does not do the owner's work (no key, no scope, no proof, no router). testing is not in the barrel: a Worker's bundle never pulls it in.

Modules

| Module | What it is | |---|---| | contract | ApiContribution, ApiOperation, ApiScopeDef, ApiCaller, OperationError, ERROR_CODES, ERROR_STATUS_CODES, defineApi, defineOperation, operationFor<Env>() (an operation whose handler sees the app's bindings), the shapes of an id, a scope and an operation id | | path | /products/{id} paths: pathProblem, compilePath, matchPath (percent-decoded parameters, one trailing slash allowed), pathsOverlap | | registry | registerApi (throws ApiContractError for a contribution that is not valid or clashes with one registered), apiContributions, checkContribution (what a feature's own test calls), clearApiContributions (tests). The registry is kept on the isolate's global object (Symbol.for("@vibedout/api-contract.contributions")), so two copies of the package in one Worker share it | | store | StoreApiContribution, StoreOperation, StoreProof, defineStoreApi, storeOperationFor<Env>(), the shapes of a proof id and header | | store-registry | registerStoreApi, storeApiContributions, storeProofsOf, checkStoreContribution, clearStoreApiContributions (the registry is on the isolate's global object under its own symbol) | | operations | the checks both kinds of operation share (operationProblems, operationClashes, errorProblems): an id, a method, a body only where the method has one, a path that reads, a schema for every {name}, no two operations a request could mean both of, refusals that make sense | | body | readJsonBody(request, maxBytes): a JSON body (application/json only, so a cross-site form post is refused) read within a ceiling that is enforced while streaming; throws BodyError for the owner to turn into its envelope | | openapi | SchemaComponents, schemaOf, parametersOf, operationObject, errorResponses, normalizeSchema, apiErrorSchema | | testing | exerciseOperation, answerProblems (test files only; @vibedout/api-contract/testing) |

registerApi is checked on purpose: a Worker that would answer with half an API, or let two features answer one path, does not start. A feature's own test calls checkContribution(catalogApi) and expects [].

Where it is tested

pnpm --filter @vibedout/api-contract test (70 tests): the paths (what is a path, what matches, an escaped slash), the registry (shared by two copies of the module, sorted, replaced by id, every thing checkContribution names, clashes of scopes, operation ids and paths between features), the types a handler is given, the storefront registry (proofs, clashes, the helpers), the OpenAPI helpers (named components, Zod 4.4 and 4.5, formats), the JSON body reader, the testing helper, and the manifest (MIT, public, Hono and Zod as peers only, every module a subpath with a build entry, testing outside the barrel).

Versions

  • 0.2.1 (50.17-T52, T49, T38): errors on an operation (OperationError, errorResponses, errorProblems; the document's x-vibedout-errors), and StoreCaller.kind and StoreCaller.allowsUrl. Additive: a contribution written for 0.2.0 works, and the blocks keep asking ^0.2.0 (the engine refuses two ranges of one package among features added together); one that uses what is new needs 0.2.1 or later, which is the first one that will be published.
  • 0.2.0: the storefront contract (defineStoreApi, buyer proofs), the OpenAPI helpers both owners share, the JSON body reader and exerciseOperation.
  • 0.1.0: the contract and registry of what a feature adds to /api/v1.