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

@thenoughtyfox/noughty-tours-web-sdk

v0.0.91

Published

Noughty Tours SDK — authenticate with your own JWTs and read your virtual tours.

Readme

@thenoughtyfox/noughty-tours-web-sdk

Embed Noughty Tours virtual tours in your own application.

The package ships two things: a typed data client for the Noughty Tours API, and React components that render the tours it returns.

The client returns plain data and renders nothing, so you can build whatever interface your product needs on top of it. If you would rather not build one, four ready-made React components cover the common screens:

| Component | Screen | | -------------- | --------------------------------------------------------- | | ToursList | A browsable list of every tour | | TourManager | Floors, rooms and scans, with editing | | Viewer | The panorama viewer, including authoring controls | | PublicViewer | The read-only viewer for a public tour, addressed by slug |

They read through the same client, so mixing the two is fine — use a component where it fits and drop to the API where it does not.

Full reference for both, including every component prop: docs/index.html.

Example apps — a complete working integration, in two halves:

| Repository | What it shows | | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | noughty-tours-web-demo | A React app wiring up the client and all four components | | noughty-tours-web-backend-demo | The backend half — mints tokens and publishes a JWK Set |

Install

npm install @thenoughtyfox/noughty-tours-web-sdk

react and react-dom are optional peer dependencies, required only for the React components.

Quick start

import { NoughtyToursController } from "@thenoughtyfox/noughty-tours-web-sdk";

const tnf = new NoughtyToursController({
  getBearerToken: async () => {
    // Call your own backend. It signs a short-lived token with your
    // private key and returns it. We never see the key.
    const res = await fetch("/api/tnf-token", { method: "POST" });
    const { access_token } = await res.json();
    return access_token;
  }
});

const tours = await tnf.tours.list();
const tour = await tnf.tours.get(tours[0].id);

Viewer configuration

Presentation settings live in viewerConfig on the client, so a host configures its look once and every viewer beneath the provider picks it up.

const tnf = new NoughtyToursController({
  getBearerToken,
  viewerConfig: {
    logoUrl: "https://your-cdn.com/logo.jpg",
    isLogoSticky: true
  }
});

| Setting | Type | Description | | -------------- | ---------------- | ------------------------------------------------------------------------------------- | | logoUrl | string \| null | Image drawn over the panorama poles. null and undefined mean no logo. | | isLogoSticky | boolean | Whether the logo turns with the visitor so it always reads upright. Defaults to true. |

A capture never covers the full sphere — the phone can see neither straight up nor straight down — so logoUrl hides that gap behind your own mark. The image must be served with permissive CORS headers: it is uploaded as a WebGL texture, and a blocked one is skipped with a console warning rather than breaking the view.

isLogoSticky: false pins the logo to the panorama instead, giving it a fixed bearing that rotates out of true as the visitor looks around.

Read the settings from your own components with useViewerConfig(). Every field is optional and additive, so new settings arrive without disturbing callers that do not use them.

For finer control over the poles — coverage angle, or capping only one — pass the poleCap prop to Viewer / PublicViewer; it overrides logoUrl.

Authentication

getBearerToken() is awaited immediately before every authenticated request and its result is sent as Authorization: Bearer <token>. Because it runs per call, it is the natural place to refresh an expired token — cache the token in your own code if you would rather not pay a round trip each time.

PublicViewer is the exception: it loads the tour from the public endpoint without a token, so visitors with no session can open a public tour.

Requests abort after 15 seconds.

Registering your issuer

Tokens are signed with your own key, and we verify them by fetching your JWK Set — so we need to know where to look. Send the Noughty Tours team your issuer (your backend's origin) and your JWKS endpoint before you start. We only fetch key sets from issuers we know about, so until that is done a correctly signed token is still rejected.

| You send us | Example | | ------------- | ------------------------------------------------ | | Issuer | https://your-backend.com | | JWKS endpoint | https://your-backend.com/.well-known/jwks.json |

Rotating a signing key needs no coordination — that is what kid is for — but tell us before you move your issuer or JWKS URL, because every token breaks until we have the new one.

Sign aud as the API origin you are calling — https://api.noughtyreality.com/tours/v1. The SDK has that origin built in, so this is the one place the value is written out by hand, and a mismatch rejects every token.

The full backend recipe, including a mintToken() example and the claims we verify, is in docs/index.html.

Resources

A NoughtyToursController exposes four resources.

| Resource | Purpose | | -------- | ---------------------------------------------- | | tours | Tours, including their floors, rooms and scans | | floors | Floor metadata and floor-plan documents | | rooms | Room metadata | | scans | Panorama pose, orientation and thumbnails |

await tnf.tours.list();
await tnf.tours.get(tourId);
await tnf.tours.getBySlug(slug);
await tnf.tours.update(tourId, { name: "Riverside apartment" });
await tnf.tours.delete(tourId);

await tnf.floors.update(floorId, { name: "Ground floor" });
await tnf.floors.delete(floorId);
await tnf.floors.uploadPlan(floorId, JSON.stringify(plan));

await tnf.rooms.update(roomId, { name: "Master bedroom", type: "bedroom" });
await tnf.rooms.delete(roomId);

await tnf.scans.update(scanId, { theta: Math.PI / 2 });
await tnf.scans.delete(scanId);
await tnf.scans.uploadThumbnail(scanId, file);

React components

Import the stylesheet once, then wrap your tree in two providers — NoughtyToursProvider supplies the client, I18nProvider supplies the language.

import {
  NoughtyToursController,
  NoughtyToursProvider,
  I18nProvider,
  ToursList
} from "@thenoughtyfox/noughty-tours-web-sdk";

import "@thenoughtyfox/noughty-tours-web-sdk/style.css";

const tnf = new NoughtyToursController({ getBearerToken });

export function App() {
  return (
    <NoughtyToursProvider client={tnf}>
      <I18nProvider defaultLocale="en">
        <ToursList onSelect={tour => navigate(`/tours/${tour.id}`)} />
      </I18nProvider>
    </NoughtyToursProvider>
  );
}

Each component fetches its own data, so you pass identifiers rather than loaded objects. They are page-level screens sized to the full window height — give each one its own route.

<ToursList onSelect={tour => navigate(`/tours/${tour.id}`)} />

<TourManager
  tourId={tourId}
  onBack={() => navigate("/tours")}
  onDeleted={() => navigate("/tours")}
  onStartTour={scanId => navigate(`/tours/${tourId}/view?scan=${scanId ?? ""}`)}
  publicURLForSlug={slug => `https://example.com/t/${slug}`}
/>

<Viewer
  tourId={tourId}
  initialScanId={scanId}
  onBackToManager={() => navigate(`/tours/${tourId}`)}
/>

<PublicViewer slug={slug} />

Styling

Every rule in the stylesheet is scoped to a .nt-root class that the components put on their own root elements. The stylesheet cannot affect the rest of your page, and your own CSS reset cannot reach inside the components. Nothing to configure, and Tailwind is not required.

Localisation

English, Romanian, Russian and German ship built in. LOCALES lists them if you want your own picker; useTranslation reads the active language.

import { LOCALES, useTranslation } from "@thenoughtyfox/noughty-tours-web-sdk";

const { locale, setLocale, t } = useTranslation();

Pass locale and onLocaleChange to I18nProvider to control the language yourself. The SDK never writes to storage — persist the choice from onLocaleChange if it should survive a reload. Override individual strings with messages:

<I18nProvider
  defaultLocale="en"
  messages={{ en: { "tour.startTour": "View property" } }}
>

Scan status

A scan only has images once processing reaches FINISHED. Filter on it before rendering — panoramaURL, panoramaLowResURL and thumbnailURL are null until then.

| Status | Meaning | | ------------ | --------------------------------------------- | | EMPTY | Created, nothing uploaded yet | | QUEUED | Uploaded and waiting to be processed | | PROCESSING | Being processed | | FINISHED | Ready; image URLs are populated | | FAILED | Processing failed; no images will be produced |

const viewable = room.scans.filter(scan => scan.status === "FINISHED");

Public tours

Setting a tour's visibility to PUBLIC mints a slug; setting it PRIVATE revokes the slug. A tour with a slug can be read without an authenticated user through getBySlug, which is what the public viewer runs on.

const updated = await tnf.tours.update(tourId, { visibility: "PUBLIC" });
// updated.slug is now set

Read slug from the update response rather than assuming its value. A link built from a revoked slug stops resolving.

Uploads

Thumbnails and floor plans go straight to storage through a presigned POST, so files never pass through the API. uploadThumbnail and uploadPlan handle both steps; getThumbnailUploadURL and getPlanUploadURL expose the presigned target if you need to drive the upload yourself.

Size limits are enforced client-side before any request is sent:

| Upload | Limit | Content type | | -------------- | ------ | ------------------ | | Scan thumbnail | 500 KB | image | | Floor plan | 1 MB | application/json |

Error handling

Any non-2xx response rejects with an ApiError.

import { ApiError } from "@thenoughtyfox/noughty-tours-web-sdk";

try {
  await tnf.tours.get(tourId);
} catch (error) {
  if (error instanceof ApiError && error.statusCode === 404) {
    // Tour does not exist, or this token cannot see it.
  }
  throw error;
}

| Property | Type | Notes | | ------------ | -------- | ----------------------------------------------------- | | message | string | Server-supplied, or "An error occurred" | | code | string | Server-supplied, or "UNKNOWN"; "TIMEOUT" on abort | | statusCode | number | HTTP status; 408 on timeout |

Uploads that exceed their size limit reject with a plain Error before the request is made.

Running the demo

demo/ holds a working end-to-end setup: a backend that mints tokens (demo/backend) and a frontend that renders the components against it (demo/landing). Run the three steps in order — the landing app needs both the built SDK and a running backend.

This demo is deliberately plain — vanilla JS and createElement, no build framework — so it shows the SDK with nothing else in the way, and it links the SDK from source so changes appear as you edit. For a conventional integration installed from npm instead, use the two public example repositories above: noughty-tours-web-demo for the frontend and noughty-tours-web-backend-demo for the backend.

1. Build the SDK

The landing app depends on the SDK through file:../../, which npm installs as a symlink to this repository. The exports map points at dist/, so there has to be a build before anything can import it.

npm install
npm run build

2. Backend — mints the tokens

cd demo/backend
npm install
npm run dev

It listens on 5050, hardcoded in demo/backend/src/main.ts — the same origin demo/landing/client.js calls. Change both together if you move it.

| Route | Purpose | | ---------------------------- | ----------------------------------------- | | POST /auth/tnf-token | Mints a short-lived ES256 JWT | | GET /.well-known/jwks.json | Publishes the public key for verification |

Making the JWKS reachable

We verify your tokens by fetching your JWKS server-side, so localhost will not resolve. Tunnel it:

ngrok http 5050

Then set issuer in demo/backend/src/config.ts to the tunnel URL — it has to match the iss claim on the tokens you mint, and is where we look for the key set. Confirm it serves:

curl https://<your-tunnel>/.well-known/jwks.json

audience and defaultOrganisationId in that same file are the API you are calling and the organisation the token acts for.

3. Landing — renders the components

cd demo/landing
npm install
npm run dev

On http://localhost:6060, with a page per component:

| Page | Component | | --------------------------------- | ------------------------------------------------------------------------------- | | / | Links to the rest | | /tours-list.html | ToursList | | /tour.html?id=<tourId> | TourManager | | /viewer.html?id=<tourId> | Viewer (add &scanId= to open a specific scan) | | /public-viewer.html?slug=<slug> | PublicViewer (add &scanId= likewise) | | /floor-plan.html?id=<tourId> | FloorPlanPage (add &floorId= to open a floor, &create=1 for a blank plan) |

Start at / and click through — ToursList links each tour to its manager, and the manager links on to the viewer and, through "Edit plan", the floor plan editor.

TypeScript

Types are exported from the package root:

import type {
  PropertyItem,
  PropertyListItem,
  Floor,
  Room,
  Scan,
  ScanStatus,
  RoomType,
  Visibility
} from "@thenoughtyfox/noughty-tours-web-sdk";