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

@mobile-surfaces/surface-contracts

v2.0.0

Published

Shared LiveSurfaceSnapshot contract, generated fixtures, and ActivityKit/APNs mapping helpers for Mobile Surfaces.

Readme

@mobile-surfaces/surface-contracts

The wire format for iOS Live Activity payloads. Works with any bridge (expo-live-activity, @mobile-surfaces/live-activity, or a hand-rolled native module), validates at the backend boundary, ships JSON Schema for non-TS validators and LLM tool use, and exposes Standard Schema so consumers can drop the Zod runtime dependency entirely.

This package is the contract. It does not depend on any iOS bridge, harness app, or push library. Pair it with whichever bridge and APNs library you already use.

Install

pnpm add @mobile-surfaces/surface-contracts

Requires Node 18+. The single runtime dependency is Zod v4.

Quick example: backend service → APNs

A typical Node service maps a domain event to a LiveSurfaceSnapshot, validates it, projects it through a kind-gated helper, and hands the result to whichever APNs library you already use:

import {
  assertSnapshot,
  toLiveActivityContentState,
  type LiveSurfaceSnapshot,
} from "@mobile-surfaces/surface-contracts";

function snapshotFromJob(job: Job): LiveSurfaceSnapshot {
  return {
    schemaVersion: "1",
    kind: "liveActivity",
    id: `${job.id}@${job.revision}`,
    surfaceId: `job-${job.id}`,
    state: job.status === "done" ? "completed" : "active",
    modeLabel: "active",
    contextLabel: job.queueName,
    statusLine: `${job.queueName} · ${Math.round(job.progress * 100)}%`,
    primaryText: job.title,
    secondaryText: job.subtitle ?? "",
    estimatedSeconds: job.etaSeconds ?? 0,
    morePartsCount: 0,
    progress: job.progress,
    stage: job.status === "done" ? "completing" : "inProgress",
    deepLink: `myapp://surface/job-${job.id}`,
  };
}

const snapshot = assertSnapshot(snapshotFromJob(job));      // throws on invalid input
const contentState = toLiveActivityContentState(snapshot);  // → { headline, subhead, progress, stage }

// Hand the projected content state to whichever APNs library you use:
await yourApnsClient.send({
  topic: `${bundleId}.push-type.liveactivity`,
  pushType: "liveactivity",
  payload: {
    aps: {
      timestamp: Math.floor(Date.now() / 1000),
      event: "update",
      "content-state": contentState,
    },
  },
});

The validator runs once at the boundary; everything downstream consumes a typed LiveSurfaceSnapshot.

The discriminated union

liveSurfaceSnapshot is a discriminated union: kind picks which branch is valid, and the validator rejects fields that belong to the wrong branch. It covers six branches at schemaVersion: "1":

| kind | Renders as | Slice | | --- | --- | --- | | liveActivity | Lock Screen Live Activity, Dynamic Island | (none, base shape) | | widget | Home-screen widget | widget: { family?, reloadPolicy? } | | control | iOS 18 control widget | control: { kind, state?, intent? } | | notification | Notification content | notification: { category?, threadId? } | | lockAccessory | Lock Screen complication (forward-compat) | (none) | | standby | StandBy mode hint (forward-compat) | (none) |

The base fields (id, surfaceId, state, modeLabel, contextLabel, statusLine, primaryText, secondaryText, actionLabel?, estimatedSeconds, morePartsCount, progress, stage, deepLink) are shared across every branch.

kind-aware narrowing is enforced both at parse time (a kind: "control" snapshot without a control slice fails safeParse) and at projection time (toLiveActivityContentState rejects a non-liveActivity snapshot at runtime):

import {
  assertSnapshot,
  toWidgetTimelineEntry,
  type LiveSurfaceSnapshot,
} from "@mobile-surfaces/surface-contracts";

const snapshot = assertSnapshot(input);

if (snapshot.kind === "widget") {
  // TS narrows: `snapshot.widget` is now LiveSurfaceWidgetSlice.
  const entry = toWidgetTimelineEntry(snapshot);
  writeAppGroupEntry(entry);
}

Projection helpers exported today: toLiveActivityContentState, toAlertPayload, toWidgetTimelineEntry, toControlValueProvider, toNotificationContentPayload. See docs/multi-surface.md for what each one returns and when to emit each kind.

Standard Schema interop

Zod 4 implements Standard Schema v1 on every exported schema, so consumers can validate against liveSurfaceSnapshot without taking a runtime dependency on Zod:

import { liveSurfaceSnapshot } from "@mobile-surfaces/surface-contracts";

const result = liveSurfaceSnapshot["~standard"].validate(input);
if (result.issues) {
  throw new Error(`Invalid snapshot: ${JSON.stringify(result.issues)}`);
}
const snapshot = result.value; // typed LiveSurfaceSnapshot

The same ~standard surface is consumable from Valibot, ArkType, @standard-schema/spec runners, any library that speaks Standard Schema. A live assertion in this package's test suite pins this behavior, so the interop is a public boundary, not an accident.

JSON Schema

Backends that aren't on TypeScript can validate against the published JSON Schema, which is generated by z.toJSONSchema and is oneOf-shaped per the discriminator. Non-TS validators get the same kind ↔ slice enforcement TypeScript consumers do.

The canonical URL is pinned to major.minor so a future minor that adds a kind branch publishes at a new URL without invalidating existing references:

https://unpkg.com/@mobile-surfaces/[email protected]/schema.json

Ajv 2020 example:

import Ajv2020 from "ajv/dist/2020.js";

const ajv = new Ajv2020();
const schema = await fetch(
  "https://unpkg.com/@mobile-surfaces/[email protected]/schema.json",
).then((r) => r.json());

const validate = ajv.compile(schema);
if (!validate(input)) {
  console.error(validate.errors);
}

The schema is also exported via the package's ./schema subpath if you want to bundle it locally:

import schema from "@mobile-surfaces/surface-contracts/schema";

v0 → v1 migration

schemaVersion: "0" was the pre-multi-projection shape (single Live-Activity-shaped object, no kind). v1 added the discriminator and per-kind slices. This schema version is separate from npm package versions like 1.2.0. Use safeParseAnyVersion at wire edges that may see either schema shape:

import { safeParseAnyVersion } from "@mobile-surfaces/surface-contracts";

const result = safeParseAnyVersion(rawBody);
if (!result.success) {
  return reject(result.error);
}

if (result.deprecationWarning) {
  log.warn(result.deprecationWarning, { snapshotId: result.data.id });
}

handle(result.data); // always v1

For stored payloads, migrateV0ToV1 is a pure transform that promotes a parsed v0 value to a v1 kind: "liveActivity" snapshot. Full migration policy and a worked example live in docs/schema-migration.md.

Pairing options

  • Use with expo-live-activity: validate at the backend boundary with assertSnapshot, project via toLiveActivityContentState, and hand the content state to your existing push library. Nothing in this package imports any bridge.
  • Use with the Mobile Surfaces starter: the harness already validates, projects, writes shared App Group state, and ships the matching widget extension. Run npm create mobile-surfaces@latest and you don't write any of the boilerplate above.
  • Use with the Mobile Surfaces push SDK: pair with @mobile-surfaces/push for an APNs client that consumes LiveSurfaceSnapshot directly: alerts, Live Activity start/update/end, push-to-start (iOS 17.2+), and broadcast channels (iOS 18+) with zero npm runtime deps.

License

MIT