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

@fastrp/phone-app-sdk

v0.3.0

Published

SDK, wire protocol and declaration contract for Fast:V Roleplay in-game phone apps.

Readme

@fastrp/phone-app-sdk

The contract between a Fast Roleplay in-game phone app and the phone that runs it: the wire protocol, the action and permission catalogue, the declaration rules, the per-app CSP, the guest SDK (vanilla and React) and the offline preview harness.

Published to npm so every consumer shares one definition:

| Consumer | Uses | | --- | --- | | App authors | @fastrp/phone-app-sdk/client or /react; dist/child.umd.js vendored into the package | | public-gateway | Serves dist/child.umd.js, dist/child.d.ts, dist/preview.html | | sites-platform, phone-service | CSP builder, declaration validation, permission catalogue | | The game's NUI host | Protocol types, PHONE_APP_ACTION_SPECS, rate limits, event names | | developers.fast-rp.com | Renders the reference tables from this package's constants |

Full developer documentation (Turkish): https://developers.fast-rp.com/docs/phone-apps.

Install

bun add @fastrp/phone-app-sdk        # or npm / pnpm

The package's major.minor tracks the protocol line it speaks (0.3.x ⇢ protocol 0.3). The version reported in the handshake is the package version, which is what the host checks.

The vendoring rule

Apps are served with script-src 'self'. A <script src="https://…/child.umd.js"> is blocked by the browser and the app silently never connects. So the UMD ships inside the app package:

  • with the starter template, bun run pack copies node_modules/@fastrp/phone-app-sdk/dist/child.umd.js to public/fastapp-sdk.js before building;
  • with a bundler and import { createPhoneSDK } from '@fastrp/phone-app-sdk/client', the SDK is simply part of your bundle and nothing needs copying.

Either way the bytes are the app's own. The host learns the SDK version during the handshake, so a broken build can still be refused centrally.

Quick start

Vanilla

<script src="./fastapp-sdk.js"></script>
<script>
  const sdk = window.PhoneSDK.createPhoneSDK();

  sdk.whenReady({ timeoutMs: 3000 }).then(
    (context) => {
      document.body.style.background = context.theme.background;
      console.log(context.installId, context.permissions);
    },
    (error) => {
      // TIMEOUT: not inside the phone. PROTOCOL_MISMATCH: update the SDK.
      console.warn(error.code);
    },
  );

  sdk.on('theme.changed', (theme) => {
    document.body.style.background = theme.background;
  });
</script>

ESM

import { createPhoneSDK, isPhoneError } from '@fastrp/phone-app-sdk/client';

const sdk = createPhoneSDK();
const context = await sdk.whenReady();

await sdk.storage.set('cart', { items: 2 });
const cart = await sdk.storage.get<{ items: number }>('cart');

try {
  await sdk.phone.notify({ title: 'Order ready' });
} catch (error) {
  if (isPhoneError(error) && error.code === 'PERMISSION_DENIED') {
    // Ask for it — the phone shows the grant sheet, the promise resolves with the new list.
    await sdk.permissions.request(['phone.notify']);
  }
}

React

import { PhoneAppProvider, usePhoneApp, usePhoneContext, usePhonePermission, usePhoneEvent }
  from '@fastrp/phone-app-sdk/react';

function App() {
  const { sdk, status } = usePhoneApp();      // 'connecting' | 'ready' | 'mismatch' | 'outside'
  const context = usePhoneContext();          // re-renders on theme/permission/viewport events
  const notify = usePhonePermission('phone.notify');

  usePhoneEvent('notification', (push) => console.log(push.title));

  if (status !== 'ready' || !context) return <p>{status}</p>;
  return (
    <button onClick={() => notify.granted ? sdk.phone.notify({ title: 'Hi' }) : notify.request()}>
      {notify.granted ? 'Notify' : 'Allow notifications'}
    </button>
  );
}

createRoot(root).render(
  <PhoneAppProvider>
    <App />
  </PhoneAppProvider>,
);

react >= 18 is an optional peer dependency; the core package has no dependencies.

What 0.3 adds

Protocol 0.3 is additive: a 0.2 app keeps working against a 0.3 phone, and the 0.3 SDK keeps every 0.2 method. New in the context (sdk.getContext() / usePhoneContext()):

| Field | Type | Live via | | --- | --- | --- | | locale | 'en' \| 'tr' — the player's game language | locale.changed | | actions | string[] — every action this host answers | — | | signal | { strength, bars, status } — cell signal | signal.changed | | launch? | { source: 'push' \| 'app', from?, data? } — why the app was opened; absent from the home screen | — |

sdk.supports('map.open') reads context.actions, so an app can hide a feature instead of catching UNKNOWN_ACTION. Against a 0.2 host (no actions field) it reports the 0.2 catalogue.

New host → guest events: locale.changed, signal.changed and storage.changed ({ key, source: 'backend' } — your backend wrote a key through the public gateway).

New actions and their SDK methods:

| Action | SDK | Permission | Consent | | --- | --- | --- | --- | | storage.clear | storage.clear() | — | — | | phone.openApp | phone.openApp({ appId, data? }) | — | once per target app per session; the target reads context.launch | | contacts.pick | contacts.pick() → { name, phone } \| null | — | the picker itself | | map.setWaypoint | map.setWaypoint(x, y, label?) | map.waypoint (new) | every call | | map.clearWaypoint | map.clearWaypoint() | map.waypoint | every call | | map.open | map.open(x, y, name?) | — | — | | environment.get | environment.get() → { hour, minute, weather, serverTime } | — | — | | browser.open | browser.open({ url }) — https://*.gta5fast.com only | — | — |

PHONE_APP_LIMITS.launchDataBytes (1 KB) caps phone.openApp data; waypointLabelChars (40) caps the waypoint label. PHONE_APP_DATA_ORIGIN and PHONE_APP_UPLOAD_ORIGIN (the public gateway and the S3 upload host) are admitted to every app's connect-src without being declared — they are what the data client below talks to.

App Data — @fastrp/phone-app-sdk/data

A backend you do not run: JSON documents in named collections, optional app accounts, and file uploads, behind the install token the phone already gives you. Collections, their JSON schema, rules (read/update/delete ∈ none|owner|app, create ∈ none|app) and indexes are configured in the developer portal; the SDK only reads and writes.

import { createDataClient, isDataError } from '@fastrp/phone-app-sdk/data';
import { sdk } from './phone';

export const data = createDataClient({ sdk });

type Order = { item: string; qty: number };
const orders = data.collection<Order>('orders');

const created = await orders.create({ item: 'latte', qty: 1 });          // Doc<Order>
await orders.update(created.id, { qty: 2 });                             // JSON merge patch
const page = await orders.list({
  where: [['qty', 'gte', 2]],
  orderBy: [['createdAt', 'desc']],
  limit: 20,
});                                                                       // { items, nextCursor }

const stop = orders.subscribe({ where: [['item', 'eq', 'latte']] }, (change) => {
  // { docId, op: 'create' | 'update' | 'delete', data | null, ownerSubject, version, at }
});

try {
  await orders.remove('someone-elses');
} catch (error) {
  if (isDataError(error) && error.code === 'FORBIDDEN') { /* the collection's rules said no */ }
}

DataError.code is one of UNAUTHORIZED | FORBIDDEN | NOT_FOUND | VALIDATION | QUOTA | RATE_LIMITED | CONFLICT | NETWORK; status and details carry what the gateway said.

Accounts and files:

await data.auth.signUp('ada', 'correct horse');   // or signIn; documents are now owned by acct:<id>
const { code } = await data.auth.createLinkCode(); // typed on another character: data.auth.link(code)
data.auth.onChange((state) => state.status);       // 'unknown' | 'anonymous' | 'authenticated'
await data.auth.signOut();

const file = await data.files.upload(blob, { name: 'receipt.png' }); // { id, url, name, mime, size }
await data.files.remove(file.id);

The session lives in sdk.storage under __fastapp_session (reserved — do not write it yourself) and rides along as X-App-Session; the install token is refreshed 60 s before it expires and never asked of the host more than once per 30 s.

React

import { DataProvider, useCollection, useDocument, useAuth } from '@fastrp/phone-app-sdk/data/react';

<PhoneAppProvider sdk={sdk}>
  <DataProvider client={data}>
    <App />
  </DataProvider>
</PhoneAppProvider>

function Orders() {
  const { items, loading, error, refetch } = useCollection<Order>(
    'orders',
    { orderBy: [['createdAt', 'desc']], limit: 50 },
    { realtime: true },       // merges SSE changes into `items` by id
  );
  const { doc } = useDocument<Order>('orders', items[0]?.id, { realtime: true });
  const auth = useAuth();     // { status, loading, account, subject, accountsEnabled, signIn, signUp, signOut, link, createLinkCode, refresh }
  …
}

Local development — the memory transport

The preview harness cannot fake /v1/data: your app runs in a frame on a different origin, so its fetch is out of the harness's reach. Fake it on your side instead:

import { createDataClient, createMemoryDataTransport } from '@fastrp/phone-app-sdk/data';

export const data = createDataClient(
  { sdk },
  { transport: import.meta.env.DEV ? createMemoryDataTransport() : undefined },
);

createMemoryDataTransport({ seed? }) answers the whole REST surface in memory — collections, query/order/paging, SSE changes, accounts, link codes, uploads — with rules app, no schema validation, no quota, and no token check (the harness's alg: none token is accepted). Data lives until the page reloads. Drop the transport to hit the real gateway.

Server side

const data = createDataClient({ credentials: { clientId, clientSecret } });

Exchanges client_credentials (phone-app:server) at <baseUrl>/oauth/token and acts as the app itself, bypassing collection rules. Never ship a client secret inside a phone app.

Reference

The action, permission, event, error-code and limit tables are rendered on the developer site from this package's constants, so they cannot drift from what the host enforces. They are not repeated here.

In code, the same tables are PHONE_APP_ACTION_SPECS, PHONE_APP_GLOBAL_RATE_LIMIT, PHONE_APP_LIMITS, PHONE_APP_PERMISSIONS, PHONE_APP_PERMISSION_CONSENT and PHONE_APP_EVENTS; every rejection is a PhoneError whose code is a PhoneAppErrorCode.

Protocol compatibility

PHONE_APP_PROTOCOL_VERSION is 0.3. The host answers ready to every line in PHONE_APP_HOST_ACCEPTS (['0.1', '0.2', '0.3']), compared major.minor with isCompatibleProtocol. A 0.1 guest ignores the event message type it does not know, and reads installId under the old characterId key; a 0.2 guest ignores the context fields and events 0.3 added — both keep working unchanged. A 0.4 guest gets PROTOCOL_MISMATCH and every call rejects rather than half-working. The host must ship 0.3 before any 0.3 app is published.

The gateway serves the same current build for /api/sdk/0.1/…, /api/sdk/0.2/… and /api/sdk/0.3/… (and any patch on each).

Two things that must not drift

script-src stays 'self'. No app ever admits an external script origin. Adding one here would add it to every app on the platform at once.

connect-src never contains https://fast-webview. That host is the NUI callback endpoint; reaching it means reaching the entire client→server event bus. declaration.ts refuses the hostname and csp.ts filters it again on the way out — a rule this load-bearing is checked on both sides of the boundary, and declaration.test.ts pins both.

Layout

src/protocol.ts          wire format: envelope, handshake, events, error codes
src/actions.ts           action catalogue, typed params/results, rate limits, PHONE_APP_LIMITS
src/permissions.ts       permission catalogue and consent modes
src/declaration.ts       the rules a declared hostname must pass before reaching connect-src
src/platform-origins.ts  the data API and upload origins every app may reach undeclared
src/csp.ts               per-app Content-Security-Policy
src/client/              the guest SDK (createPhoneSDK, PhoneError)
src/react/               PhoneAppProvider and hooks
src/data/                the App Data client (createDataClient, DataError, createMemoryDataTransport)
src/data/react/          DataProvider, useCollection, useDocument, useAuth
scripts/build.ts         bundles dist/ (see below) and emits declarations
scripts/prepare-publish.ts swaps the workspace `exports` for the dist map before `npm publish`
preview.html             standalone harness that mocks the phone in a browser

Build and publish

bun run build          # dist/index.js, client.js, react.js, data.js, data-react.js, child.umd.js, child.d.ts, preview.html, *.d.ts
bun run check-types
bun test

In the workspace, exports resolves to src/ for Bun (bun condition) and TypeScript (types), so no build is needed to type-check or run the other packages. publishConfig.exports holds the dist/ map; .github/workflows/pkg-phone-app-sdk.yml swaps it in and runs npm publish when the version in package.json is not yet on the registry. Bumping the version is the release.

public-gateway reads dist/ at boot, so its Dockerfile runs this build; locally run it once before starting the gateway.