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

@game-infra/story-schemas

v0.5.4

Published

Story-mode API contract: branching arcs and beats built on the event-editor's encounter-stage and choice DTOs, behind one design-content resource

Readme

@game-infra/story-schemas

API contract for authored design content and story mode: valibot schemas plus toad-contracts defineApiContract definitions. Covers worlds, the content written against them (locations, characters, skill definitions, lore entries), the story arcs chosen for a world, and the branching beats those arcs are told in. Builds on @game-infra/api-schemas-core for the shared success-response wrapper.

Published to npm. Also consumable as sibling source (exports points at src/).

Install

// pnpm sibling checkout
"@game-infra/story-schemas": "link:../../game-infra/packages/schemas/story-schemas"
// or from npm
"@game-infra/story-schemas": "^0.1.0"

valibot is a peer dependency (^1.4.0); the consumer provides it. @game-infra/api-schemas-core is a runtime dependency (re-uses createSuccessResponseSchema).

One resource, not a family

All of it is the same shape — a keyed JSON payload, scoped to a parent, tagged with a type — so it is one resource with five endpoints rather than a CRUD family per kind. contentType and scopeId do the work a separate resource per kind would otherwise do:

| contentType | scopeId | contentId | | ------------------------------------------------------------------------------------ | ---------- | ----------------------- | | world | - | the world id | | locations, characters, events, stats, skills, perks, resources, lore | a world id | the entity id | | story-arc | a world id | the story id | | story-node | a story id | the trail of choice ids |

data is an opaque JSON string. The service stores and indexes it, never parses it — a game's design vocabulary changes far more often than this contract should, which is the same rule GameEntitySaveRequestSchema already follows in api-schemas-core. contentType is likewise plain string(); DESIGN_CONTENT_TYPES is the suggested vocabulary, not a closed one.

Usage

import { designContentListContract, designContentLoadContract } from "@game-infra/story-schemas";
import { mapApiContractToPath } from "@toad-contracts/valibot";

designContentListContract.pathResolver({ gameId: "g1" }); // '/games/g1/design-content'
mapApiContractToPath(designContentLoadContract);
// '/games/:gameId/design-content/:contentType/:scopeId/:contentId'

Register on a Hono service with @toad-contracts/hono (buildHonoRoute) and call from a frontend with @toad-contracts/frontend-http-client (sendByApiContract) — both drive off the same contract objects.

The five endpoints

| Contract | Method + path | What it is for | | ------------------------------- | ------------------------------------------- | -------------------------------------------------- | | designContentListContract | GET /games/:gameId/design-content | every listing, narrowed by contentType/scopeId | | designContentLoadContract | GET …/:contentType/:scopeId/:contentId | one row by key | | designContentSaveContract | POST /games/:gameId/design-content | upsert one row | | designContentBulkSaveContract | POST /games/:gameId/design-content/bulk | upsert many, optionally as a snapshot | | designContentDeleteContract | DELETE …/:contentType/:scopeId/:contentId | delete one row, with an optional cascade |

The listing answers every question story mode asks:

// the worlds to choose between
{ contentType: "world" }
// a world's whole design document, payloads included
{ scopeId: "the-ash-reckoning", includeData: "true" }
// the arcs offered for that world
{ contentType: "story-arc", scopeId: "the-ash-reckoning" }
// every beat already written for an arc — the map of what is pregenerated
{ contentType: "story-node", scopeId: "the-quiet-forge-answers", includeData: "true" }

includeData is off by default: a picker needs names and counts, not every character's biography.

Snapshots and cascades

designContentBulkSaveContract takes a replace list. Each entry clears a scope (or one type within it) before the write, which is what makes a bulk save a snapshot rather than a merge — without it, content the author deleted locally would survive on the server forever.

designContentDeleteContract takes ?cascade=:

  • none (default) — the row alone.
  • prefix — the row and every row whose contentId sits under it. Discarding a story branch takes its descendants with it this way.
  • scope — the row and everything scoped to it, transitively. Deleting a world also removes its content, its arcs, and their beats.

Payload shapes — the event editor's, not new ones

A branching story and a multi-stage event are the same object seen from two angles: both are prose with gated, consequential branches out of it. So a story beat is an EncounterStageDTO and a branch is a ChoiceDefinitionDTO, straight from @game-infra/event-schemas:

| Story mode | event-editor DTO | | ---------- | --------------------------------------------------------------------------------------------------------------------- | | arc | EventDefinitionDTO header (multi-stage) + hook/tags + story-specific player power, stat values, and skill ranks | | beat | EncounterStageDTO + model/origin/revision/notes | | branch | ChoiceDefinitionDTO, unchanged |

A branch therefore carries conditionsToShow, conditionsToEnable, consumableConditions, effects and an outcome config exactly as an event's choice does, and a finished arc assembles into an event_definitions row by collecting its beats into stages — a copy, not a translation.

data is opaque to the service, but two clients writing and reading the same rows still have to agree, so the shapes ship alongside the contract and are parsed client-side:

import { StoryBeatPayloadSchema } from "@game-infra/story-schemas";
import { safeParse } from "valibot";

const parsed = safeParse(StoryBeatPayloadSchema, JSON.parse(row.data ?? "{}"));
if (parsed.success) console.log(parsed.output.description, parsed.output.choices);

WorldPayloadSchema, StoryArcPayloadSchema and StoryBeatPayloadSchema are exported for this, along with the underlying ChoiceDefinitionDTOSchema, PreconditionDTOSchema, ActivationDTOSchema and ConsumableConditionDTOSchema so a story client needs one import rather than two. They are deliberately not attached to any endpoint.

Use the lenient DTOs, not the strict ones. StrictPreconditionDTOSchema and StrictActivationDTOSchema enumerate one game's vocabulary (strength, mana, LearnSpell, SpecificLecture). A design world declares its own stats, skills, perks and resources, so a story client uses the lenient { type, params } forms and validates type and the ids in params against the world it belongs to.

What this package is not for

Everything here is authored — written once, read by every player, and the same tomorrow as today. Anything that changes because someone is playing — a session in progress, its journal of turns, a save slot — belongs to @game-infra/game-state-schemas and game-state-service instead. Freeform mode's turn journal and synapse memory live there, in FreeformTurnPayloadSchema and friends.

Beat addressing

A beat's identity is the trail of choices taken to reach it — that is what makes "has this branch been travelled before?" a lookup rather than a search. The client minting ids and the service sweeping subtrees import the same helpers:

import { storyNodeId, childStoryNodeId, isStoryNodeAtOrBelow } from "@game-infra/story-schemas";

storyNodeId([]); // 'root'
childStoryNodeId("root", "take-the-tunnel"); // 'root.take-the-tunnel'
isStoryNodeAtOrBelow("root.take.left", "root.take"); // true
isStoryNodeAtOrBelow("root.taken", "root.take"); // false

Segments join with . rather than / because a beat id travels as a single URL path segment, which a slash would split; the dot is also what cascade=prefix sweeps on.

Extension points

  • contentType: typed string(); DESIGN_CONTENT_TYPES is the suggested vocabulary and a game re-tightens it in its own validation layer.
  • data: opaque to the contract, so a game evolves its design document without a contract bump. Add a payload schema next to the existing three when a new type needs one.
  • origin: a beat is generated or authored — the latter is a branch a player wrote themselves.
  • Condition and effect vocabulary: type is free text in the lenient DTOs, so each game declares the set it evaluates and drops the rest.

Dependencies

@game-infra/api-schemas-core (the success-response wrapper), @game-infra/event-schemas (the stage and choice DTOs), @toad-contracts/core, @toad-contracts/valibot; peer valibot.

Consumers

services/game-content-service (the Hono service registering these routes) and the design/story clients that call them — currently the Fluid Emerald game editor.