npm package: @safercity/sdk
Description: Official SaferCity API client for TypeScript/JavaScript
Author: SaferCity
Latest version: 0.4.0

# @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.

```tsx
// 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

```bash
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.

```typescript
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](#proxy-middleware) below).

### Direct Mode

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

```typescript
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.

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

## Quick Start

```typescript
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:

```typescript
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

```typescript
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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
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:

```typescript
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

```typescript
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:

```typescript
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:

```bash
# 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`:

```typescript
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