@safercity/sdk-react 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,9 +10,31 @@ npm install @safercity/sdk-react @tanstack/react-query
10
10
  bun add @safercity/sdk-react @tanstack/react-query
11
11
  ```
12
12
 
13
- ## Quick Start
13
+ ## Authentication Modes
14
14
 
15
- ### 1. Wrap Your App with Provider
15
+ The provider supports three authentication modes. Choose the one that fits your architecture:
16
+
17
+ ### Proxy Mode (Default - Most Secure)
18
+
19
+ Client -> Your Backend -> SaferCity API. Your backend handles credentials.
20
+
21
+ ```tsx
22
+ import { SaferCityProvider } from '@safercity/sdk-react';
23
+
24
+ function App() {
25
+ return (
26
+ <SaferCityProvider mode="proxy" proxyBaseUrl="/api/safercity">
27
+ <YourApp />
28
+ </SaferCityProvider>
29
+ );
30
+ }
31
+ ```
32
+
33
+ Set up the proxy on your backend with `createNextHandler` or `createExpressMiddleware` from `@safercity/sdk`.
34
+
35
+ ### Direct Mode
36
+
37
+ Client -> SaferCity API with an external auth token. For white-label apps using Clerk, Auth0, better-auth, etc.
16
38
 
17
39
  ```tsx
18
40
  import { SaferCityProvider } from '@safercity/sdk-react';
@@ -20,9 +42,10 @@ import { SaferCityProvider } from '@safercity/sdk-react';
20
42
  function App() {
21
43
  return (
22
44
  <SaferCityProvider
45
+ mode="direct"
23
46
  baseUrl="https://api.safercity.com"
24
- token={userToken}
25
47
  tenantId="tenant-123"
48
+ getAccessToken={() => session?.accessToken}
26
49
  >
27
50
  <YourApp />
28
51
  </SaferCityProvider>
@@ -30,7 +53,84 @@ function App() {
30
53
  }
31
54
  ```
32
55
 
33
- ### 2. Use Hooks in Components
56
+ The provider automatically refreshes the token every 30 seconds by calling `getAccessToken`.
57
+
58
+ ### Cookie Mode
59
+
60
+ Browser with `credentials: include`. For first-party web apps using session cookies.
61
+
62
+ ```tsx
63
+ import { SaferCityProvider } from '@safercity/sdk-react';
64
+
65
+ function App() {
66
+ return (
67
+ <SaferCityProvider mode="cookie" baseUrl="https://api.safercity.com">
68
+ <YourApp />
69
+ </SaferCityProvider>
70
+ );
71
+ }
72
+ ```
73
+
74
+ The provider automatically checks session status on mount via `/v1/auth/session/status`.
75
+
76
+ ### Legacy Mode
77
+
78
+ For backward compatibility, you can still pass `baseUrl` and `token` directly without a `mode`:
79
+
80
+ ```tsx
81
+ <SaferCityProvider baseUrl="https://api.safercity.com" token={userToken} tenantId="tenant-123">
82
+ <YourApp />
83
+ </SaferCityProvider>
84
+ ```
85
+
86
+ ## Session Management (Cookie Mode)
87
+
88
+ When using cookie mode, the provider exposes session management hooks:
89
+
90
+ ```tsx
91
+ import { useSession, useSessionManager, useAuthMode } from '@safercity/sdk-react';
92
+
93
+ function AuthStatus() {
94
+ const session = useSession();
95
+ const mode = useAuthMode(); // "proxy" | "direct" | "cookie"
96
+
97
+ if (session.isLoading) return <div>Loading...</div>;
98
+ if (session.error) return <div>Error: {session.error.message}</div>;
99
+
100
+ return <div>Authenticated: {session.isAuthenticated ? 'Yes' : 'No'}</div>;
101
+ }
102
+
103
+ function LoginButton() {
104
+ const { createSession, clearSession, refreshCsrf, isAuthenticated } = useSessionManager();
105
+
106
+ const handleLogin = async (externalToken: string) => {
107
+ await createSession(externalToken, 'tenant-123');
108
+ };
109
+
110
+ const handleLogout = async () => {
111
+ await clearSession();
112
+ };
113
+
114
+ return isAuthenticated
115
+ ? <button onClick={handleLogout}>Logout</button>
116
+ : <button onClick={() => handleLogin('...')}>Login</button>;
117
+ }
118
+ ```
119
+
120
+ ### SessionState
121
+
122
+ ```typescript
123
+ interface SessionState {
124
+ isAuthenticated: boolean;
125
+ isLoading: boolean;
126
+ userId?: string;
127
+ tenantId?: string;
128
+ expiresAt?: number;
129
+ error?: Error;
130
+ }
131
+ ```
132
+
133
+ ## Using Hooks
34
134
 
35
135
  ```tsx
36
136
  import { useUsers, usePanics, useCreatePanic } from '@safercity/sdk-react';
@@ -68,32 +168,16 @@ function PanicButton({ userId }: { userId: string }) {
68
168
  }
69
169
  ```
70
170
 
71
- ## Streaming Hook
72
-
73
- ```tsx
74
- import { usePanicStream } from '@safercity/sdk-react';
75
-
76
- function PanicTracker({ panicId }: { panicId: string }) {
77
- const { data, isConnected, error, events } = usePanicStream(panicId, {
78
- keepHistory: true,
79
- onEvent: (event) => console.log('Update:', event),
80
- });
81
-
82
- if (error) return <div>Error: {error.message}</div>;
83
- if (!isConnected) return <div>Connecting...</div>;
84
-
85
- return (
86
- <div>
87
- <p>Latest: {data?.data}</p>
88
- <p>Total events: {events.length}</p>
89
- </div>
90
- );
91
- }
92
- ```
93
-
94
171
  ## Available Hooks
95
172
 
96
- ### Health & Auth
173
+ ### Provider Hooks
174
+ - `useSaferCity()` - Full context (client, mode, session, session management)
175
+ - `useSaferCityClient()` - SaferCity client instance
176
+ - `useSession()` - Session state (cookie mode)
177
+ - `useAuthMode()` - Current auth mode (`"proxy"` | `"direct"` | `"cookie"`)
178
+ - `useSessionManager()` - Session management functions (cookie mode)
179
+
180
+ ### Health and Auth
97
181
  - `useHealthCheck()` - API health status
98
182
  - `useWhoAmI()` - Current auth context
99
183
 
@@ -126,12 +210,36 @@ function PanicTracker({ panicId }: { panicId: string }) {
126
210
  - `useCrimeCategories()` - Get crime categories
127
211
  - `useCrimeTypes()` - Get crime types
128
212
 
213
+ ## Streaming Hook
214
+
215
+ ```tsx
216
+ import { usePanicStream } from '@safercity/sdk-react';
217
+
218
+ function PanicTracker({ panicId }: { panicId: string }) {
219
+ const { data, isConnected, error, events } = usePanicStream(panicId, {
220
+ keepHistory: true,
221
+ onEvent: (event) => console.log('Update:', event),
222
+ });
223
+
224
+ if (error) return <div>Error: {error.message}</div>;
225
+ if (!isConnected) return <div>Connecting...</div>;
226
+
227
+ return (
228
+ <div>
229
+ <p>Latest: {data?.data}</p>
230
+ <p>Total events: {events.length}</p>
231
+ </div>
232
+ );
233
+ }
234
+ ```
235
+
129
236
  ## Query Keys
130
237
 
131
238
  Use query keys for manual cache management:
132
239
 
133
240
  ```tsx
134
- import { saferCityKeys, useQueryClient } from '@safercity/sdk-react';
241
+ import { saferCityKeys } from '@safercity/sdk-react';
242
+ import { useQueryClient } from '@tanstack/react-query';
135
243
 
136
244
  function RefreshButton() {
137
245
  const queryClient = useQueryClient();
@@ -164,11 +272,7 @@ const queryClient = new QueryClient({
164
272
 
165
273
  function App() {
166
274
  return (
167
- <SaferCityProvider
168
- baseUrl="https://api.safercity.com"
169
- token={token}
170
- queryClient={queryClient}
171
- >
275
+ <SaferCityProvider mode="proxy" proxyBaseUrl="/api/safercity" queryClient={queryClient}>
172
276
  <YourApp />
173
277
  </SaferCityProvider>
174
278
  );
@@ -184,7 +288,6 @@ function CustomComponent() {
184
288
  const client = useSaferCityClient();
185
289
 
186
290
  const customRequest = async () => {
187
- // Use raw client for custom requests
188
291
  const response = await client._client.get('/custom-endpoint');
189
292
  return response.data;
190
293
  };
package/dist/index.cjs CHANGED
@@ -6,20 +6,187 @@ var reactQuery = require('@tanstack/react-query');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
 
8
8
  var SaferCityContext = react.createContext(null);
9
- function SaferCityProvider({
10
- children,
11
- queryClient: externalQueryClient,
12
- ...clientOptions
13
- }) {
14
- const client = react.useMemo(
15
- () => sdk.createSaferCityClient(clientOptions),
16
- // Only recreate if baseUrl or tenantId changes
17
- // Token updates should use client.setToken()
18
- [clientOptions.baseUrl, clientOptions.tenantId]
19
- );
20
- react.useMemo(() => {
21
- client.setToken(clientOptions.token);
22
- }, [client, clientOptions.token]);
9
+ function isProxyMode(props) {
10
+ return !("mode" in props) || props.mode === "proxy" || props.mode === void 0;
11
+ }
12
+ function isDirectMode(props) {
13
+ return "mode" in props && props.mode === "direct";
14
+ }
15
+ function isCookieMode(props) {
16
+ return "mode" in props && props.mode === "cookie";
17
+ }
18
+ function isLegacyMode(props) {
19
+ return "baseUrl" in props && "token" in props && !("mode" in props);
20
+ }
21
+ function SaferCityProvider(props) {
22
+ const { children, queryClient: externalQueryClient } = props;
23
+ const mode = react.useMemo(() => {
24
+ if (isDirectMode(props)) return "direct";
25
+ if (isCookieMode(props)) return "cookie";
26
+ return "proxy";
27
+ }, [props]);
28
+ const [session, setSession] = react.useState({
29
+ isAuthenticated: false,
30
+ isLoading: mode === "cookie"
31
+ });
32
+ const client = react.useMemo(() => {
33
+ if (isLegacyMode(props)) {
34
+ return sdk.createSaferCityClient({
35
+ baseUrl: props.baseUrl,
36
+ token: props.token,
37
+ tenantId: props.tenantId,
38
+ fetch: props.fetch,
39
+ timeout: props.timeout,
40
+ headers: props.headers
41
+ });
42
+ }
43
+ if (isProxyMode(props)) {
44
+ const proxyBaseUrl = props.proxyBaseUrl ?? "/api/safercity";
45
+ return sdk.createSaferCityClient({
46
+ baseUrl: proxyBaseUrl
47
+ });
48
+ }
49
+ if (isDirectMode(props)) {
50
+ return sdk.createSaferCityClient({
51
+ baseUrl: props.baseUrl,
52
+ tenantId: props.tenantId
53
+ });
54
+ }
55
+ if (isCookieMode(props)) {
56
+ return sdk.createSaferCityClient({
57
+ baseUrl: props.baseUrl,
58
+ tenantId: props.tenantId,
59
+ headers: {
60
+ // Note: cookies are sent automatically with fetch credentials: 'include'
61
+ }
62
+ });
63
+ }
64
+ throw new Error("Invalid SaferCityProvider configuration");
65
+ }, [props]);
66
+ const directModeTokenGetter = isDirectMode(props) ? props.getAccessToken : null;
67
+ react.useEffect(() => {
68
+ if (!directModeTokenGetter) return;
69
+ let isMounted = true;
70
+ const updateToken = async () => {
71
+ const token = await directModeTokenGetter();
72
+ if (isMounted) {
73
+ client.setToken(token);
74
+ }
75
+ };
76
+ updateToken();
77
+ const interval = setInterval(updateToken, 3e4);
78
+ return () => {
79
+ isMounted = false;
80
+ clearInterval(interval);
81
+ };
82
+ }, [client, directModeTokenGetter]);
83
+ react.useEffect(() => {
84
+ if (mode !== "cookie") return;
85
+ if (!isCookieMode(props)) return;
86
+ let isMounted = true;
87
+ const checkSession = async () => {
88
+ try {
89
+ const response = await fetch(`${props.baseUrl}/v1/auth/session/status`, {
90
+ credentials: "include"
91
+ });
92
+ if (!isMounted) return;
93
+ if (response.ok) {
94
+ const data = await response.json();
95
+ setSession({
96
+ isAuthenticated: data.authenticated,
97
+ isLoading: false,
98
+ userId: data.userId,
99
+ tenantId: data.tenantId,
100
+ expiresAt: data.expiresAt
101
+ });
102
+ } else {
103
+ setSession({
104
+ isAuthenticated: false,
105
+ isLoading: false
106
+ });
107
+ }
108
+ } catch (error) {
109
+ if (!isMounted) return;
110
+ setSession({
111
+ isAuthenticated: false,
112
+ isLoading: false,
113
+ error: error instanceof Error ? error : new Error("Session check failed")
114
+ });
115
+ }
116
+ };
117
+ checkSession();
118
+ return () => {
119
+ isMounted = false;
120
+ };
121
+ }, [mode, props]);
122
+ const createSession = react.useCallback(async (token, tenantId) => {
123
+ if (mode !== "cookie" || !isCookieMode(props)) {
124
+ throw new Error("createSession is only available in cookie mode");
125
+ }
126
+ setSession((prev) => ({ ...prev, isLoading: true, error: void 0 }));
127
+ try {
128
+ const response = await fetch(`${props.baseUrl}/v1/auth/session`, {
129
+ method: "POST",
130
+ credentials: "include",
131
+ headers: {
132
+ "Content-Type": "application/json",
133
+ ...tenantId && { "X-Tenant-ID": tenantId }
134
+ },
135
+ body: JSON.stringify({ token, tenantId })
136
+ });
137
+ if (!response.ok) {
138
+ const error = await response.json();
139
+ throw new Error(error.message || "Failed to create session");
140
+ }
141
+ const data = await response.json();
142
+ setSession({
143
+ isAuthenticated: true,
144
+ isLoading: false,
145
+ expiresAt: data.expiresAt,
146
+ tenantId
147
+ });
148
+ } catch (error) {
149
+ setSession({
150
+ isAuthenticated: false,
151
+ isLoading: false,
152
+ error: error instanceof Error ? error : new Error("Failed to create session")
153
+ });
154
+ throw error;
155
+ }
156
+ }, [mode, props]);
157
+ const clearSession = react.useCallback(async () => {
158
+ if (mode !== "cookie" || !isCookieMode(props)) {
159
+ throw new Error("clearSession is only available in cookie mode");
160
+ }
161
+ try {
162
+ await fetch(`${props.baseUrl}/v1/auth/session/logout`, {
163
+ method: "POST",
164
+ credentials: "include"
165
+ });
166
+ } finally {
167
+ setSession({
168
+ isAuthenticated: false,
169
+ isLoading: false
170
+ });
171
+ }
172
+ }, [mode, props]);
173
+ const refreshCsrf = react.useCallback(async () => {
174
+ if (mode !== "cookie" || !isCookieMode(props)) {
175
+ throw new Error("refreshCsrf is only available in cookie mode");
176
+ }
177
+ try {
178
+ const response = await fetch(`${props.baseUrl}/v1/auth/session/refresh`, {
179
+ method: "POST",
180
+ credentials: "include"
181
+ });
182
+ if (response.ok) {
183
+ const data = await response.json();
184
+ return data.csrfToken;
185
+ }
186
+ } catch {
187
+ }
188
+ return void 0;
189
+ }, [mode, props]);
23
190
  const queryClient = react.useMemo(
24
191
  () => externalQueryClient ?? new reactQuery.QueryClient({
25
192
  defaultOptions: {
@@ -38,7 +205,14 @@ function SaferCityProvider({
38
205
  }),
39
206
  [externalQueryClient]
40
207
  );
41
- const contextValue = react.useMemo(() => ({ client }), [client]);
208
+ const contextValue = react.useMemo(() => ({
209
+ client,
210
+ mode,
211
+ session,
212
+ createSession,
213
+ clearSession,
214
+ refreshCsrf
215
+ }), [client, mode, session, createSession, clearSession, refreshCsrf]);
42
216
  return /* @__PURE__ */ jsxRuntime.jsx(reactQuery.QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsxRuntime.jsx(SaferCityContext.Provider, { value: contextValue, children }) });
43
217
  }
44
218
  function useSaferCity() {
@@ -53,6 +227,23 @@ function useSaferCity() {
53
227
  function useSaferCityClient() {
54
228
  return useSaferCity().client;
55
229
  }
230
+ function useSession() {
231
+ return useSaferCity().session;
232
+ }
233
+ function useAuthMode() {
234
+ return useSaferCity().mode;
235
+ }
236
+ function useSessionManager() {
237
+ const { createSession, clearSession, refreshCsrf, session } = useSaferCity();
238
+ return {
239
+ session,
240
+ createSession,
241
+ clearSession,
242
+ refreshCsrf,
243
+ isAuthenticated: session.isAuthenticated,
244
+ isLoading: session.isLoading
245
+ };
246
+ }
56
247
  var saferCityKeys = {
57
248
  all: ["safercity"],
58
249
  // Health
@@ -471,6 +662,7 @@ function useStream(createStream, options = {}) {
471
662
 
472
663
  exports.SaferCityProvider = SaferCityProvider;
473
664
  exports.saferCityKeys = saferCityKeys;
665
+ exports.useAuthMode = useAuthMode;
474
666
  exports.useCancelPanic = useCancelPanic;
475
667
  exports.useCreatePanic = useCreatePanic;
476
668
  exports.useCreateSubscription = useCreateSubscription;
@@ -486,6 +678,8 @@ exports.usePanicStream = usePanicStream;
486
678
  exports.usePanics = usePanics;
487
679
  exports.useSaferCity = useSaferCity;
488
680
  exports.useSaferCityClient = useSaferCityClient;
681
+ exports.useSession = useSession;
682
+ exports.useSessionManager = useSessionManager;
489
683
  exports.useStream = useStream;
490
684
  exports.useSubscriptionStats = useSubscriptionStats;
491
685
  exports.useSubscriptionTypes = useSubscriptionTypes;