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

@safercity/sdk

v0.4.0

Published

Official SaferCity API client for TypeScript/JavaScript

Readme

@safercity/sdk

Official SaferCity API client for TypeScript/JavaScript.

What's New in v0.4.0

  • 100% Codegen Types - All request bodies, query params, and response types in client.ts and server.ts now reference the generated OpenAPI types from types.gen.ts. Zero inline/manual type literals remain. Future API changes auto-propagate via bun run generate:all.
  • Subscription SDK Realignment - Removed client.subscriptions.subscribeUser() (dead route). Updated client.subscriptions.create() to use isPremium flag. Added lifecycle methods to ServerClient: getByUser(), update(), and switch().
  • Panic SDK Enhancements - Added client.panics.createPremium() for CellFind provider integration and client.panics.eligibility() for pre-panic validation. Expanded updateLocation() and cancel() with additional metadata fields.
  • Breaking Changes - subscriptions.subscribeUser() removed. subscriptions.create() body shape changed to isPremium-based.

What's New in v0.3.2

  • Proxy Mode Enhanced - ProxyModeConfig now supports tenantId, userId, and custom headers. White-label apps can pass tenant context through the proxy without exposing credentials.
  • Banner API Fix - Fixed banner.get() parameter from radius to days to match the API schema.
// New proxy mode with tenant support
<SaferCityProvider
  mode="proxy"
  proxyBaseUrl="/api/safercity"
  tenantId="tenant-123"        // Sent as X-Tenant-ID header
  userId="user-456"            // For user-scoped operations
  headers={{ 'X-App': 'my-app' }}
>
  <App />
</SaferCityProvider>

What's New in v0.3.1

  • Simplified Return Types - Domain methods now return the API response body directly instead of wrapping in ApiResponse<T>. For example, client.health.check() returns Promise<{ status: string; timestamp: string }> directly. No more result.data.data.id — just result.data.id.
  • Strongly Typed Errors - All SDK methods throw SaferCityApiError on non-2xx responses with typed status, error, message, and details fields. Error handling is unchanged from v0.3.0.
  • Low-Level Access Preserved - client._client.get() and ServerClient.get() still return the full ApiResponse<T> with { data, status, headers } for cases where you need HTTP metadata.

What's New in v0.3.0

  • Typed SDK via OpenAPI Codegen - All request/response types are now auto-generated from the API's OpenAPI spec using @hey-api/openapi-ts. This guarantees 1:1 type alignment between the SDK and API — no more manual type mismatches.
  • Re-exported Generated Types - All generated API types (request bodies, response shapes, error types) are re-exported from @safercity/sdk for direct use in your application.
  • Correct Field Names - Request bodies now use the exact API field names (emailAddress not email, panicType not panicTypeId, phoneNumber not phone).
  • Simplified Return Types - Domain methods now return the API response body directly instead of wrapping in ApiResponse<T>. Access result.data.id instead of result.data.data.id.
  • Richer Response Types - Response types now include the full API response structure (e.g., { success: boolean; data: {...}; message?: string }).

What's New in v0.2.0

  • User-Scoped Client - The main client is now user-scoped (Stripe publishable key pattern). Admin operations moved to ServerClient.
  • Panic Information - Full CRUD for user panic profiles and emergency contacts.
  • Proxy Enhancements - Added allowlist/blocklist support for proxy endpoints.
  • Automatic Scoping - Client now tracks userId and automatically scopes requests.
  • Path Alignment - All SDK paths now match the latest API schema (singular /v1/panic, etc.).

What's New in v0.1.3

  • OAuth endpoint path fix - Fixed paths (/oauth/*/v1/oauth/*)
  • ServerClient domain helpers - Added typed domain helpers to ServerClient
  • Security hardening - Removed panics.list() and subscriptions.stats() from client-side SDK (available on ServerClient only)

Installation

npm install @safercity/sdk
# or
bun add @safercity/sdk
# or
yarn add @safercity/sdk

Authentication Modes

The SDK supports three authentication modes for different deployment scenarios:

Proxy Mode (Default - Most Secure)

Client -> Your Backend -> SaferCity API. Your backend adds tenant credentials, keeping secrets out of the client.

import { createSaferCityClient } from '@safercity/sdk';

// Client-side: requests go through your proxy
const client = createSaferCityClient({
  baseUrl: '/api/safercity',
});

Set up the proxy on your backend (see Proxy Middleware below).

Direct Mode

Client -> SaferCity API with an external user token. For white-label apps using external auth (Clerk, Auth0, better-auth).

const client = createSaferCityClient({
  baseUrl: 'https://api.safercity.com',
  token: externalAuthToken,
  tenantId: 'your-tenant-id',
  userId: 'user-123', // optional, for auto-scoping
});

// Update token or user when they change
client.setToken(newToken);
client.setUserId(newUserId);

Cookie Mode

Browser with credentials: include. For first-party web apps using session cookies.

const client = createSaferCityClient({
  baseUrl: 'https://api.safercity.com',
  tenantId: 'your-tenant-id',
  userId: 'user-123', // optional
});

Quick Start

import { createSaferCityClient } from '@safercity/sdk';

const client = createSaferCityClient({
  baseUrl: 'https://api.safercity.com',
  token: 'your-jwt-token',
  tenantId: 'your-tenant-id',
});

// Health check
const health = await client.health.check();
console.log('API Status:', health.status);

// Get user (auto-resolves userId from client if not passed)
const user = await client.users.get();
console.log('User:', user.data.firstName);

// Create panic (userId is optional if set on client)
const panic = await client.panics.create({
  userId: 'user-123',
  panicType: 'emergency',
  latitude: -26.2041,
  longitude: 28.0473,
});

console.log('Panic created:', panic.data.id);

Server Client

For backend applications that need automatic OAuth token management:

import { createServerClient } from '@safercity/sdk';

const client = createServerClient({
  auth: {
    clientId: process.env.SAFERCITY_CLIENT_ID!,
    clientSecret: process.env.SAFERCITY_CLIENT_SECRET!,
    tenantId: 'tenant-123', // optional
  },
  baseUrl: 'https://api.safercity.com', // default
  timeout: 30000,
});

// All requests are automatically authenticated with OAuth tokens
// Tokens are refreshed automatically before expiration
const users = await client.users.list(); // Admin only
const panic = await client.panics.create({
  userId: 'user-123',
  panicType: 'emergency',
  latitude: -26.2041,
  longitude: 28.0473,
});


// ServerClient has ALL endpoints including admin-only ones:
// - client.oauth.*
// - client.tenants.*
// - client.credentials.*
// - client.users.list()
// - client.users.delete()
// - client.users.updateStatus()
// - client.panics.list()
// - client.subscriptions.getByUser(userId)
// - client.subscriptions.update(id, body)
// - client.subscriptions.switch(id, body)
// - client.panicInformation.list()

// Low-level requests still work too
const response = await client.get('/v1/custom-endpoint');

// Manual token control
const token = await client.getAccessToken();
await client.refreshToken();
client.clearTokens();

Note: The ServerClient includes panics.list() and subscriptions.stats() which are not available on the client-side SDK for security reasons.

ServerClientConfig

interface ServerClientConfig {
  baseUrl?: string;           // default: "https://api.safercity.com"
  auth: OAuthCredentials;     // { clientId, clientSecret, tenantId? }
  tokenStore?: TokenStorage;  // custom token storage (default: in-memory)
  timeout?: number;           // default: 30000
  fetch?: typeof fetch;       // custom fetch implementation
}

Proxy Middleware

Proxy middleware hides your SaferCity credentials from the client. The client sends requests to your backend, which forwards them to SaferCity with proper authentication.

Next.js App Router

// app/api/safercity/[...path]/route.ts
import { createNextHandler } from '@safercity/sdk';

const handler = createNextHandler({
  clientId: process.env.SAFERCITY_CLIENT_ID!,
  clientSecret: process.env.SAFERCITY_CLIENT_SECRET!,
  tenantId: 'tenant-123', // optional, can also come from request header
});

export { handler as GET, handler as POST, handler as PUT, handler as DELETE, handler as PATCH };

Express

import express from 'express';
import { createExpressMiddleware } from '@safercity/sdk';

const app = express();

app.use(
  '/api/safercity',
  createExpressMiddleware({
    clientId: process.env.SAFERCITY_CLIENT_ID!,
    clientSecret: process.env.SAFERCITY_CLIENT_SECRET!,
  })
);

Generic Handler

import { createProxyHandler } from '@safercity/sdk';

const proxy = createProxyHandler({
  clientId: process.env.SAFERCITY_CLIENT_ID!,
  clientSecret: process.env.SAFERCITY_CLIENT_SECRET!,
});

// Use with any framework
const response = await proxy({
  method: 'GET',
  path: '/v1/users',
  headers: {},
  query: { limit: '10' },
});

ProxyConfig

interface ProxyConfig {
  clientId: string;
  clientSecret: string;
  baseUrl?: string;            // default: "https://api.safercity.com"
  tenantId?: string;           // optional, can be extracted from request
  tokenStore?: TokenStorage;   // custom token storage
  pathPrefix?: string;         // default: "/api/safercity"
  forwardHeaders?: string[];   // default: ["content-type", "accept", "x-request-id"]
  fetch?: typeof fetch;
  allowedEndpoints?: EndpointPattern[]; // Explicit allowlist
  blockedEndpoints?: EndpointPattern[]; // Explicit blocklist
}

type EndpointPattern = string | { method?: string; path: string };

Allowlist takes precedence over blocklist. Patterns match by path prefix (case-insensitive).

Streaming (SSE)

Stream real-time panic updates:

for await (const event of client.panics.streamUpdates('panic-123')) {
  console.log('Update:', JSON.parse(event.data));
}

API Reference

Health

  • client.health.check() - Check API status

Authentication

  • client.auth.whoami() - Get current auth context
  • client.auth.check() - Check if authenticated (optional auth)

Users (User-Scoped)

  • client.users.create(body) - Create user (emailAddress, firstName, lastName, phoneNumber?, password?)
  • client.users.get(userId?) - Get user by ID (defaults to client's userId)
  • client.users.update(userId?, body) - Update user (emailAddress?, firstName?, lastName?, phoneNumber?, password?)

Panics

  • client.panics.create(body) - Create panic
  • client.panics.createPremium(body) - Create premium panic with CellFind provider
  • client.panics.get(panicId, query?) - Get panic
  • client.panics.updateLocation(panicId, body) - Update location (supports accuracy, speed, batteryLevel, etc.)
  • client.panics.cancel(panicId, body) - Cancel panic (supports reason, cancelledByUser)
  • client.panics.eligibility(userId?) - Check panic eligibility (subscription + profile)
  • client.panics.types(userId?) - Get available panic types for a user
  • client.panics.streamUpdates(panicId, options?) - Stream updates (SSE)

Panic Information

  • client.panicInformation.create(body) - Create panic profile (userId, phoneNumber, firstName, lastName, idNumber, duressCode, emergencyContacts?)
  • client.panicInformation.get(id) - Get profile by ID
  • client.panicInformation.getByUser(userId?) - Get profile by user ID
  • client.panicInformation.update(id, body) - Update profile
  • client.panicInformation.delete(id) - Delete profile
  • client.panicInformation.validateEligibility(userId?) - Check if user is eligible for panic services

Subscriptions

  • client.subscriptions.listTypes() - List subscription types
  • client.subscriptions.create(body) - Create subscription (userId, isPremium, startDate?, endDate?, notes?)
  • client.subscriptions.list(query?) - List subscriptions (supports status, subscriptionTypeId, sortBy, search, etc.)

Notifications

  • client.notifications.createSubscriber(body) - Create subscriber
  • client.notifications.trigger(body) - Trigger notification
  • client.notifications.bulkTrigger(body) - Bulk trigger notifications
  • client.notifications.getPreferences(userId?) - Get user preferences
  • client.notifications.updatePreferences(userId?, body) - Update preferences

Location Safety

  • client.locationSafety.check(body) - Check location safety (POST)

Banner

  • client.banner.get(body) - Get crime banner data for a location

Crimes

  • client.crimes.list(query?) - List crimes
  • client.crimes.categories() - Get crime categories
  • client.crimes.types() - Get crime types
  • client.crimes.categoriesWithTypes() - Get categories with nested types

Error Handling

import { SaferCityApiError } from '@safercity/sdk';

try {
  await client.users.get('invalid-id');
} catch (error) {
  if (error instanceof SaferCityApiError) {
    console.log('API Error:', error.error);
    console.log('Message:', error.message);
    console.log('Status:', error.status);
  }
}

Custom Fetch

For environments with custom fetch requirements:

import { createSaferCityClient } from '@safercity/sdk';

const client = createSaferCityClient({
  baseUrl: 'https://api.safercity.com',
  fetch: customFetchImplementation,
});

Type Generation

The SDK types are auto-generated from the API's OpenAPI specification using @hey-api/openapi-ts.

Regenerating Types

If the API schema changes, regenerate the types:

# Start the API server locally first
bun run dev

# Fetch the latest OpenAPI spec and regenerate types
bun run -C packages/sdk/client generate:all

# Or step by step:
bun run -C packages/sdk/client fetch:openapi  # Fetches from http://localhost:3000/openapi
bun run -C packages/sdk/client generate        # Generates types from openapi.json

Using Generated Types

All generated types are re-exported from @safercity/sdk:

import type {
  UserCreateBody,
  PanicCreatedResponse,
  CreatePanicBody,
  GetV1UsersByUserIdResponse,
  PanicInformationResponse,
} from '@safercity/sdk';

Related Packages

  • @safercity/sdk-react - React hooks with TanStack Query
  • @safercity/sdk-react-native - React Native support with expo-fetch

License

MIT