@safercity/sdk
v0.4.0
Published
Official SaferCity API client for TypeScript/JavaScript
Maintainers
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.tsandserver.tsnow reference the generated OpenAPI types fromtypes.gen.ts. Zero inline/manual type literals remain. Future API changes auto-propagate viabun run generate:all. - Subscription SDK Realignment - Removed
client.subscriptions.subscribeUser()(dead route). Updatedclient.subscriptions.create()to useisPremiumflag. Added lifecycle methods toServerClient:getByUser(),update(), andswitch(). - Panic SDK Enhancements - Added
client.panics.createPremium()for CellFind provider integration andclient.panics.eligibility()for pre-panic validation. ExpandedupdateLocation()andcancel()with additional metadata fields. - Breaking Changes -
subscriptions.subscribeUser()removed.subscriptions.create()body shape changed toisPremium-based.
What's New in v0.3.2
- Proxy Mode Enhanced -
ProxyModeConfignow supportstenantId,userId, and customheaders. White-label apps can pass tenant context through the proxy without exposing credentials. - Banner API Fix - Fixed
banner.get()parameter fromradiustodaysto 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()returnsPromise<{ status: string; timestamp: string }>directly. No moreresult.data.data.id— justresult.data.id. - Strongly Typed Errors - All SDK methods throw
SaferCityApiErroron non-2xx responses with typedstatus,error,message, anddetailsfields. Error handling is unchanged from v0.3.0. - Low-Level Access Preserved -
client._client.get()andServerClient.get()still return the fullApiResponse<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/sdkfor direct use in your application. - Correct Field Names - Request bodies now use the exact API field names (
emailAddressnotemail,panicTypenotpanicTypeId,phoneNumbernotphone). - Simplified Return Types - Domain methods now return the API response body directly instead of wrapping in
ApiResponse<T>. Accessresult.data.idinstead ofresult.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
userIdand 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()andsubscriptions.stats()from client-side SDK (available onServerClientonly)
Installation
npm install @safercity/sdk
# or
bun add @safercity/sdk
# or
yarn add @safercity/sdkAuthentication 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
ServerClientincludespanics.list()andsubscriptions.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 contextclient.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'suserId)client.users.update(userId?, body)- Update user (emailAddress?,firstName?,lastName?,phoneNumber?,password?)
Panics
client.panics.create(body)- Create panicclient.panics.createPremium(body)- Create premium panic with CellFind providerclient.panics.get(panicId, query?)- Get panicclient.panics.updateLocation(panicId, body)- Update location (supportsaccuracy,speed,batteryLevel, etc.)client.panics.cancel(panicId, body)- Cancel panic (supportsreason,cancelledByUser)client.panics.eligibility(userId?)- Check panic eligibility (subscription + profile)client.panics.types(userId?)- Get available panic types for a userclient.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 IDclient.panicInformation.getByUser(userId?)- Get profile by user IDclient.panicInformation.update(id, body)- Update profileclient.panicInformation.delete(id)- Delete profileclient.panicInformation.validateEligibility(userId?)- Check if user is eligible for panic services
Subscriptions
client.subscriptions.listTypes()- List subscription typesclient.subscriptions.create(body)- Create subscription (userId,isPremium,startDate?,endDate?,notes?)client.subscriptions.list(query?)- List subscriptions (supportsstatus,subscriptionTypeId,sortBy,search, etc.)
Notifications
client.notifications.createSubscriber(body)- Create subscriberclient.notifications.trigger(body)- Trigger notificationclient.notifications.bulkTrigger(body)- Bulk trigger notificationsclient.notifications.getPreferences(userId?)- Get user preferencesclient.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 crimesclient.crimes.categories()- Get crime categoriesclient.crimes.types()- Get crime typesclient.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.jsonUsing 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
