een-api-toolkit 0.3.16 → 0.3.22

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.
Files changed (53) hide show
  1. package/.claude/agents/docs-accuracy-reviewer.md +146 -0
  2. package/.claude/agents/een-auth-agent.md +168 -0
  3. package/.claude/agents/een-devices-agent.md +294 -0
  4. package/.claude/agents/een-events-agent.md +375 -0
  5. package/.claude/agents/een-media-agent.md +256 -0
  6. package/.claude/agents/een-setup-agent.md +126 -0
  7. package/.claude/agents/een-users-agent.md +239 -0
  8. package/.claude/agents/test-runner.md +144 -0
  9. package/CHANGELOG.md +151 -10
  10. package/README.md +1 -0
  11. package/dist/index.cjs +3 -1
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +561 -0
  14. package/dist/index.js +483 -260
  15. package/dist/index.js.map +1 -1
  16. package/docs/AI-CONTEXT.md +128 -1648
  17. package/docs/ai-reference/AI-AUTH.md +288 -0
  18. package/docs/ai-reference/AI-DEVICES.md +569 -0
  19. package/docs/ai-reference/AI-EVENTS.md +1745 -0
  20. package/docs/ai-reference/AI-MEDIA.md +974 -0
  21. package/docs/ai-reference/AI-SETUP.md +267 -0
  22. package/docs/ai-reference/AI-USERS.md +255 -0
  23. package/examples/vue-event-subscriptions/.env.example +15 -0
  24. package/examples/vue-event-subscriptions/README.md +103 -0
  25. package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
  26. package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
  27. package/examples/vue-event-subscriptions/index.html +13 -0
  28. package/examples/vue-event-subscriptions/package-lock.json +1726 -0
  29. package/examples/vue-event-subscriptions/package.json +29 -0
  30. package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
  31. package/examples/vue-event-subscriptions/src/App.vue +193 -0
  32. package/examples/vue-event-subscriptions/src/composables/useHlsPlayer.ts +272 -0
  33. package/examples/vue-event-subscriptions/src/main.ts +25 -0
  34. package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
  35. package/examples/vue-event-subscriptions/src/stores/connection.ts +101 -0
  36. package/examples/vue-event-subscriptions/src/stores/mediaSession.ts +79 -0
  37. package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
  38. package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
  39. package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +901 -0
  40. package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
  41. package/examples/vue-event-subscriptions/src/views/Logout.vue +65 -0
  42. package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +389 -0
  43. package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
  44. package/examples/vue-event-subscriptions/tsconfig.json +21 -0
  45. package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
  46. package/examples/vue-event-subscriptions/vite.config.ts +12 -0
  47. package/examples/vue-events/package-lock.json +8 -1
  48. package/examples/vue-events/package.json +1 -0
  49. package/examples/vue-events/src/components/EventsModal.vue +269 -47
  50. package/examples/vue-events/src/composables/useHlsPlayer.ts +272 -0
  51. package/examples/vue-events/src/stores/mediaSession.ts +79 -0
  52. package/package.json +10 -2
  53. package/scripts/setup-agents.ts +116 -0
@@ -1,303 +1,161 @@
1
1
  # EEN API Toolkit - AI Reference
2
2
 
3
- > **Version:** 0.3.16
3
+ > **Version:** 0.3.22
4
4
  >
5
- > This file is optimized for AI assistants. It contains all API signatures,
6
- > types, and usage patterns in a single, parseable document.
5
+ > This documentation is optimized for AI assistants. It provides focused, domain-specific
6
+ > references to help you understand and use the een-api-toolkit efficiently.
7
7
  >
8
- > For the full EEN API documentation, see the
9
- > [Eagle Eye Networks Developer Portal](https://developer.eagleeyenetworks.com).
10
-
11
- > **Working Examples:** The installed package includes complete Vue 3 example applications
12
- > at `./node_modules/een-api-toolkit/examples/`. These demonstrate OAuth authentication,
13
- > user management, camera listing, live/recorded media, video feeds, events, and metrics.
14
- > For Live Main Video streaming, see `vue-feeds/src/views/Feeds.vue`.
15
- > For Events API with thumbnails, see `vue-events/src/components/EventsModal.vue`.
16
- > For Event Metrics with Chart.js visualization, see `vue-alerts-metrics/src/components/MetricsChart.vue`.
8
+ > **Split Documentation:** Load only the documents you need to preserve context.
17
9
 
18
10
  ---
19
11
 
20
- ## Prerequisites & Installation (READ FIRST)
21
-
22
- > **⚠️ CRITICAL:** This section contains essential setup requirements.
23
- > Skipping these steps will cause runtime errors.
24
-
25
- ### Prerequisites
26
-
27
- Before using the een-api-toolkit, ensure you have:
28
-
29
- | Requirement | Details |
30
- |-------------|---------|
31
- | **Vue 3.x** | The toolkit is built for Vue 3 Composition API |
32
- | **Pinia** | Required peer dependency for state management |
33
- | **Vite** | Recommended build tool (dev server must run on `127.0.0.1:3333`) |
34
- | **OAuth Proxy** | Required for secure token management (see [een-oauth-proxy](https://github.com/klaushofrichter/een-oauth-proxy)) |
35
-
36
- ### Installation
37
-
38
- ```bash
39
- npm install een-api-toolkit pinia
40
- ```
41
-
42
- ### Complete Setup (main.ts)
43
-
44
- > **⚠️ CRITICAL:** Pinia MUST be installed on the Vue app BEFORE calling
45
- > `initEenToolkit()` or using `useAuthStore()`. Failure to do so will cause
46
- > a runtime error.
47
-
48
- ```typescript
49
- // main.ts - Complete Setup Example
50
- import { createApp } from 'vue'
51
- import { createPinia } from 'pinia'
52
- import { initEenToolkit } from 'een-api-toolkit'
53
- import App from './App.vue'
54
- import router from './router'
55
-
56
- const app = createApp(App)
57
- const pinia = createPinia()
58
-
59
- // Step 1: Install Pinia FIRST (required)
60
- app.use(pinia)
61
-
62
- // Step 2: Install router if using Vue Router
63
- app.use(router)
64
-
65
- // Step 3: Initialize the toolkit (Pinia must already be installed)
66
- initEenToolkit({
67
- proxyUrl: import.meta.env.VITE_PROXY_URL, // e.g., 'http://localhost:8787'
68
- clientId: import.meta.env.VITE_EEN_CLIENT_ID, // Your EEN OAuth client ID
69
- redirectUri: 'http://127.0.0.1:3333', // Must be exactly this
70
- debug: import.meta.env.VITE_DEBUG === 'true'
71
- })
72
-
73
- // Step 4: Mount the app
74
- app.mount('#app')
75
- ```
76
-
77
- ### Environment Variables (.env)
78
-
79
- ```env
80
- VITE_PROXY_URL=http://localhost:8787
81
- VITE_EEN_CLIENT_ID=your-een-client-id
82
- VITE_DEBUG=true
83
- ```
84
-
85
- ### Common Errors
86
-
87
- #### "getActivePinia() was called but there was no active Pinia"
88
-
89
- **Cause:** Pinia was not installed before `initEenToolkit()` was called or before using `useAuthStore()`.
90
-
91
- **Solution:** Ensure your `main.ts` calls `app.use(pinia)` BEFORE `initEenToolkit()`:
92
-
93
- ```typescript
94
- const app = createApp(App)
95
- const pinia = createPinia()
96
-
97
- app.use(pinia) // ✅ First - install Pinia
98
- initEenToolkit({...}) // ✅ Second - initialize toolkit
99
- app.mount('#app') // ✅ Last - mount app
100
- ```
101
-
102
- #### "Redirect URI mismatch"
103
-
104
- **Cause:** OAuth redirect URI doesn't exactly match `http://127.0.0.1:3333`.
105
-
106
- **Solution:**
107
- - Use `127.0.0.1` not `localhost`
108
- - Use port `3333` exactly
109
- - No trailing slash (not `http://127.0.0.1:3333/`)
110
- - No path (not `http://127.0.0.1:3333/callback`)
111
- - Register this exact URI at [EEN Developer Portal](https://developer.eagleeyenetworks.com/page/my-application)
112
-
113
- **Vite config:**
114
- ```typescript
115
- // vite.config.ts
116
- server: { host: '127.0.0.1', port: 3333, strictPort: true }
117
- ```
118
-
119
- **Router pattern:** Handle OAuth callback on root path, then redirect internally:
120
- ```typescript
121
- // router/index.ts - root path must catch OAuth params
122
- {
123
- path: '/',
124
- beforeEnter: (to, _from, next) => {
125
- if (to.query.code && to.query.state) {
126
- next({ name: 'callback', query: to.query })
127
- } else {
128
- next()
129
- }
130
- }
131
- }
132
- ```
12
+ ## Document Navigation
133
13
 
134
- ### Common Pitfalls - Preview Images
14
+ | Task | Document | Tokens |
15
+ |------|----------|--------|
16
+ | Setting up a new Vue 3 app | [AI-SETUP.md](./ai-reference/AI-SETUP.md) | ~2.5K |
17
+ | Implementing OAuth login | [AI-AUTH.md](./ai-reference/AI-AUTH.md) | ~2K |
18
+ | Working with users | [AI-USERS.md](./ai-reference/AI-USERS.md) | ~1.5K |
19
+ | Working with cameras or bridges | [AI-DEVICES.md](./ai-reference/AI-DEVICES.md) | ~3K |
20
+ | Live video, images, HLS playback | [AI-MEDIA.md](./ai-reference/AI-MEDIA.md) | ~4K |
21
+ | Events, alerts, metrics, SSE | [AI-EVENTS.md](./ai-reference/AI-EVENTS.md) | ~3.5K |
135
22
 
136
- Displaying live preview images requires careful attention to authentication. Here are common mistakes:
23
+ ## Specialized Agents
137
24
 
138
- #### DON'T: Construct API URLs directly for `<img>` tags
25
+ Specialized agents are available in `.claude/agents/` for domain-specific tasks:
139
26
 
140
- ```typescript
141
- // WRONG - browsers cannot send Authorization headers with <img src>
142
- const url = `${authStore.baseUrl}/api/v3.0/media/liveImage.jpeg?deviceId=${cameraId}`
143
- imgElement.src = url // Results in 401 Unauthorized
144
- ```
27
+ | Agent | Use When |
28
+ |-------|----------|
29
+ | `een-setup-agent` | Creating a new Vue 3 app, fixing Pinia errors, OAuth redirect issues |
30
+ | `een-auth-agent` | Implementing login/logout, auth callbacks, route guards, token refresh |
31
+ | `een-users-agent` | Listing users, user profiles, user management features |
32
+ | `een-devices-agent` | Working with cameras or bridges, filtering by status/tags |
33
+ | `een-media-agent` | Live video, camera previews, HLS playback, recorded images |
34
+ | `een-events-agent` | Events, alerts, metrics, real-time SSE subscriptions |
145
35
 
146
- **Why it fails:** The `<img>` element makes a simple GET request without custom headers. The JWT token cannot be sent, so the request is unauthorized.
36
+ **How to Use Agents:**
147
37
 
148
- #### DON'T: Modify multipartUrl by adding query parameters
38
+ 1. **In the toolkit repository:** Agents are automatically discovered in `.claude/agents/`
149
39
 
150
- ```typescript
151
- // WRONG - adding parameters breaks the pre-signed URL
152
- const feedUrl = feed.multipartUrl
153
- imgElement.src = `${feedUrl}?timestamp=${Date.now()}` // Results in 400 Bad Request
154
- imgElement.src = `${feedUrl}&cache=${Date.now()}` // Also fails
155
- ```
40
+ 2. **In your own project using een-api-toolkit:**
41
+ ```bash
42
+ # Install agents to your project (user must run this command)
43
+ npx een-setup-agents
44
+ ```
45
+ This copies agents to `.claude/agents/` where Claude Code discovers them automatically.
156
46
 
157
- **Why it fails:** The `multipartUrl` is a complete, pre-authenticated URL. Any modification (including cache-busting parameters) invalidates it.
47
+ 3. **Manual access:** Agents can also be read directly from `node_modules/een-api-toolkit/.claude/agents/`
158
48
 
159
- #### DO: Use `getLiveImage()` for preview thumbnails
49
+ **For AI assistants:**
160
50
 
161
- ```typescript
162
- // CORRECT - returns base64 data URL that works directly in <img src>
163
- import { getLiveImage } from 'een-api-toolkit'
51
+ > **IMPORTANT:** If specialized agents are not installed in this project's `.claude/agents/` directory,
52
+ > recommend that the user run `npx een-setup-agents` to install them. This enables automatic
53
+ > agent discovery and provides focused, context-efficient assistance for EEN API development.
164
54
 
165
- const { data, error } = await getLiveImage({ deviceId: cameraId })
166
- if (data?.imageData) {
167
- imgElement.src = data.imageData // "data:image/jpeg;base64,..."
168
- }
55
+ To use an agent's knowledge without installation, read its definition directly:
169
56
  ```
170
-
171
- **Why it works:** The toolkit handles authentication internally and returns a base64-encoded data URL that can be used directly without browser restrictions.
172
-
173
- ### Choosing the Right Preview Method
174
-
175
- | Use Case | Method | Notes |
176
- |----------|--------|-------|
177
- | Grid of camera thumbnails (20+) | `getLiveImage()` | Best for large grids, handles auth internally |
178
- | Periodic refresh (e.g., every 3s) | `getLiveImage()` | Call repeatedly in a timer |
179
- | Camera grid (<20 cameras) | `multipartUrl` | Automatic updates via continuous MJPEG stream. Requires `initMediaSession()` first |
180
- | One-time snapshot | `getLiveImage()` | Simple and self-contained |
181
- | Full-quality live video | Live Video SDK | For modal/fullscreen video playback |
182
-
183
- **Quick Reference:**
184
-
185
- ```typescript
186
- // For thumbnails and grids - USE THIS
187
- const { data } = await getLiveImage({ deviceId })
188
- img.src = data.imageData
189
-
190
- // For continuous MJPEG stream (grids <20 cameras, auto-updates)
191
- await initMediaSession()
192
- const { data: feeds } = await listFeeds({ deviceId, include: ['multipartUrl'] })
193
- img.src = feeds.results.find(f => f.type === 'preview')?.multipartUrl
194
-
195
- // For HD live video - use @een/live-video-web-sdk
196
- const player = new LivePlayer()
197
- player.start({ videoElement, cameraId, baseUrl, jwt })
57
+ Read: node_modules/een-api-toolkit/.claude/agents/een-auth-agent.md
198
58
  ```
59
+ Then follow the context files and instructions specified within.
199
60
 
200
- ---
201
-
202
- ## Quick Reference
203
-
204
- ### Example Applications
205
-
206
- Complete Vue 3 applications demonstrating toolkit features:
61
+ ## Example Applications
207
62
 
208
63
  | Example | Description | Key Files |
209
64
  |---------|-------------|-----------|
210
65
  | [vue-users](../examples/vue-users/) | User management with pagination | `src/views/Users.vue` |
211
66
  | [vue-cameras](../examples/vue-cameras/) | Camera listing with status filters | `src/views/Cameras.vue` |
212
67
  | [vue-bridges](../examples/vue-bridges/) | Bridge listing with device info | `src/views/Bridges.vue` |
213
- | [vue-media](../examples/vue-media/) | Live and recorded image viewing | `src/views/LiveCamera.vue`, `RecordedImage.vue`, `HLS.vue` |
214
- | [vue-feeds](../examples/vue-feeds/) | Live video streaming (preview and main) | `src/views/Feeds.vue` |
215
- | [vue-events](../examples/vue-events/) | Events with bounding box overlays | `src/components/EventsModal.vue` |
216
- | [vue-alerts-metrics](../examples/vue-alerts-metrics/) | Event metrics, alerts, and notifications | `src/components/MetricsChart.vue`, `AlertsList.vue` |
68
+ | [vue-media](../examples/vue-media/) | Live and recorded image viewing | `src/views/LiveCamera.vue` |
69
+ | [vue-feeds](../examples/vue-feeds/) | Live video streaming | `src/views/Feeds.vue` |
70
+ | [vue-events](../examples/vue-events/) | Events with bounding boxes | `src/components/EventsModal.vue` |
71
+ | [vue-alerts-metrics](../examples/vue-alerts-metrics/) | Event metrics and alerts | `src/components/MetricsChart.vue` |
72
+ | [vue-event-subscriptions](../examples/vue-event-subscriptions/) | Real-time SSE streaming | `src/views/LiveEvents.vue` |
217
73
 
218
- ### Configuration
74
+ ---
219
75
 
76
+ ## Quick Reference - All Functions
77
+
78
+ ### Configuration
220
79
  | Function | Purpose |
221
80
  |----------|---------|
222
81
  | `initEenToolkit(config)` | Initialize the toolkit with proxy URL and client ID |
223
82
 
224
- ### Authentication Functions
225
-
226
- | Function | Purpose | Returns |
227
- |----------|---------|---------|
228
- | `getAuthUrl()` | Generate OAuth authorization URL | `string` |
229
- | `handleAuthCallback(code, state)` | Exchange auth code for token | `Result<TokenResponse>` |
230
- | `refreshToken()` | Refresh the access token | `Result<{accessToken, expiresIn}>` |
231
- | `revokeToken()` | Revoke token and logout | `Result<void>` |
232
-
233
- ### User Functions
234
-
235
- | Function | Purpose | Returns |
236
- |----------|---------|---------|
237
- | `getCurrentUser()` | Get current user profile | `Result<UserProfile>` |
238
- | `getUsers(params?)` | List all users (paginated) | `Result<PaginatedResult<User>>` |
239
- | `getUser(userId, params?)` | Get a specific user | `Result<User>` |
240
-
241
- ### Camera Functions
242
-
243
- | Function | Purpose | Returns |
244
- |----------|---------|---------|
245
- | `getCameras(params?)` | List all cameras (paginated) | `Result<PaginatedResult<Camera>>` |
246
- | `getCamera(cameraId, params?)` | Get a specific camera | `Result<Camera>` |
247
-
248
- ### Bridge Functions
249
-
250
- | Function | Purpose | Returns |
251
- |----------|---------|---------|
252
- | `getBridges(params?)` | List all bridges (paginated) | `Result<PaginatedResult<Bridge>>` |
253
- | `getBridge(bridgeId, params?)` | Get a specific bridge | `Result<Bridge>` |
254
-
255
- ### Media Functions
256
-
257
- | Function | Purpose | Returns |
258
- |----------|---------|---------|
259
- | `listMedia(params)` | List media intervals for a device | `Result<PaginatedResult<MediaInterval>>` |
260
- | `listFeeds(params)` | List available feeds for a device | `Result<ListFeedsResult>` |
261
- | `getLiveImage(params)` | Get live preview image from camera | `Result<LiveImageResult>` |
262
- | `getRecordedImage(params)` | Get recorded image from history | `Result<RecordedImageResult>` |
263
- | `getMediaSession()` | Get media session URL for cookies | `Result<MediaSessionResponse>` |
264
- | `initMediaSession()` | Initialize media session (sets cookie) | `Result<MediaSessionResult>` |
265
-
266
- ### Events Functions
83
+ ### Authentication
84
+ | Function | Purpose |
85
+ |----------|---------|
86
+ | `getAuthUrl()` | Generate OAuth authorization URL |
87
+ | `handleAuthCallback(code, state)` | Exchange auth code for token |
88
+ | `refreshToken()` | Refresh the access token |
89
+ | `revokeToken()` | Revoke token and logout |
267
90
 
268
- | Function | Purpose | Returns |
269
- |----------|---------|---------|
270
- | `listEvents(params)` | List events for a device with filters | `Result<PaginatedResult<Event>>` |
271
- | `getEvent(eventId, params?)` | Get a specific event by ID | `Result<Event>` |
272
- | `listEventTypes(params?)` | List all available event types | `Result<PaginatedResult<EventType>>` |
273
- | `listEventFieldValues(params)` | Get available event types for a device | `Result<EventFieldValues>` |
91
+ ### Users
92
+ | Function | Purpose |
93
+ |----------|---------|
94
+ | `getCurrentUser()` | Get current user profile |
95
+ | `getUsers(params?)` | List all users (paginated) |
96
+ | `getUser(userId, params?)` | Get a specific user |
274
97
 
275
- ### Event Metrics Functions
98
+ ### Cameras
99
+ | Function | Purpose |
100
+ |----------|---------|
101
+ | `getCameras(params?)` | List all cameras (paginated) |
102
+ | `getCamera(cameraId, params?)` | Get a specific camera |
276
103
 
277
- | Function | Purpose | Returns |
278
- |----------|---------|---------|
279
- | `getEventMetrics(params)` | Get event count metrics over time | `Result<EventMetric[]>` |
104
+ ### Bridges
105
+ | Function | Purpose |
106
+ |----------|---------|
107
+ | `getBridges(params?)` | List all bridges (paginated) |
108
+ | `getBridge(bridgeId, params?)` | Get a specific bridge |
280
109
 
281
- ### Alerts Functions
110
+ ### Media
111
+ | Function | Purpose |
112
+ |----------|---------|
113
+ | `listMedia(params)` | List media intervals for a device |
114
+ | `listFeeds(params)` | List available feeds for a device |
115
+ | `getLiveImage(params)` | Get live preview image from camera |
116
+ | `getRecordedImage(params)` | Get recorded image from history |
117
+ | `getMediaSession()` | Get media session URL for cookies |
118
+ | `initMediaSession()` | Initialize media session (sets cookie) |
119
+
120
+ ### Events
121
+ | Function | Purpose |
122
+ |----------|---------|
123
+ | `listEvents(params)` | List events for a device |
124
+ | `getEvent(eventId, params?)` | Get a specific event |
125
+ | `listEventTypes(params?)` | List all available event types |
126
+ | `listEventFieldValues(params)` | Get event types for a device |
282
127
 
283
- | Function | Purpose | Returns |
284
- |----------|---------|---------|
285
- | `listAlerts(params?)` | List alerts with filters | `Result<PaginatedResult<Alert>>` |
286
- | `getAlert(id, params?)` | Get a specific alert by ID | `Result<Alert>` |
287
- | `listAlertTypes(params?)` | List all available alert types | `Result<PaginatedResult<AlertType>>` |
128
+ ### Event Metrics
129
+ | Function | Purpose |
130
+ |----------|---------|
131
+ | `getEventMetrics(params)` | Get event count metrics over time |
288
132
 
289
- ### Notifications Functions
133
+ ### Alerts
134
+ | Function | Purpose |
135
+ |----------|---------|
136
+ | `listAlerts(params?)` | List alerts with filters |
137
+ | `getAlert(id, params?)` | Get a specific alert |
138
+ | `listAlertTypes(params?)` | List all available alert types |
290
139
 
291
- | Function | Purpose | Returns |
292
- |----------|---------|---------|
293
- | `listNotifications(params?)` | List notifications with filters | `Result<PaginatedResult<Notification>>` |
294
- | `getNotification(id)` | Get a specific notification by ID | `Result<Notification>` |
140
+ ### Notifications
141
+ | Function | Purpose |
142
+ |----------|---------|
143
+ | `listNotifications(params?)` | List notifications |
144
+ | `getNotification(id)` | Get a specific notification |
295
145
 
296
- ### Utility Functions
146
+ ### Event Subscriptions
147
+ | Function | Purpose |
148
+ |----------|---------|
149
+ | `listEventSubscriptions(params?)` | List all subscriptions |
150
+ | `getEventSubscription(id)` | Get a specific subscription |
151
+ | `createEventSubscription(params)` | Create a new subscription |
152
+ | `deleteEventSubscription(id)` | Delete a subscription |
153
+ | `connectToEventSubscription(sseUrl, options)` | Connect to SSE stream |
297
154
 
298
- | Function | Purpose | Returns |
299
- |----------|---------|---------|
300
- | `formatTimestamp(timestamp)` | Convert ISO timestamp from Z to +00:00 format (required by EEN API) | `string` |
155
+ ### Utilities
156
+ | Function | Purpose |
157
+ |----------|---------|
158
+ | `formatTimestamp(timestamp)` | Convert Z to +00:00 format |
301
159
 
302
160
  ---
303
161
 
@@ -349,1320 +207,15 @@ interface PaginatedResult<T> {
349
207
  }
350
208
  ```
351
209
 
352
- ### Configuration Type
353
-
354
- ```typescript
355
- type StorageStrategy = 'localStorage' | 'sessionStorage' | 'memory'
356
-
357
- interface EenToolkitConfig {
358
- proxyUrl?: string // OAuth proxy URL (required for API calls)
359
- clientId?: string // EEN OAuth client ID
360
- redirectUri?: string // OAuth redirect URI (default: http://127.0.0.1:3333)
361
- storageStrategy?: StorageStrategy // Token storage: 'localStorage' (default), 'sessionStorage', or 'memory'
362
- debug?: boolean // Enable debug logging
363
- }
364
- ```
365
-
366
- ### Storage Strategy Descriptions
367
-
368
- Human-readable descriptions for each storage strategy, useful for displaying in UI components:
369
-
370
- ```typescript
371
- import { getStorageStrategy, STORAGE_STRATEGY_DESCRIPTIONS } from 'een-api-toolkit'
372
-
373
- // STORAGE_STRATEGY_DESCRIPTIONS is a Record<StorageStrategy, string>:
374
- // {
375
- // localStorage: 'persists across sessions',
376
- // sessionStorage: 'per-tab, cleared on tab close',
377
- // memory: 'tokens lost on page refresh'
378
- // }
379
-
380
- const strategy = getStorageStrategy()
381
- const description = STORAGE_STRATEGY_DESCRIPTIONS[strategy]
382
- console.log(`Using ${strategy}: ${description}`)
383
- ```
384
-
385
- ---
386
-
387
- ## Entity Types
388
-
389
- ### User
390
-
391
- ```typescript
392
- interface User {
393
- id: string
394
- email: string
395
- firstName: string
396
- lastName: string
397
- accountId?: string
398
- timeZone?: string // IANA timezone (e.g., "America/Los_Angeles")
399
- language?: string // ISO 639-1 code (e.g., "en")
400
- phone?: string
401
- mobilePhone?: string
402
- permissions?: string[] // Requires include: ['permissions']
403
- lastLogin?: string // ISO 8601 timestamp
404
- isActive?: boolean
405
- createdAt?: string
406
- updatedAt?: string
407
- }
408
-
409
- interface UserProfile {
410
- id: string
411
- email: string
412
- firstName: string
413
- lastName: string
414
- accountId?: string
415
- timeZone?: string
416
- language?: string
417
- }
418
- ```
419
-
420
- ### Camera
421
-
422
- ```typescript
423
- type CameraStatus =
424
- | 'online' | 'offline' | 'deviceOffline' | 'bridgeOffline'
425
- | 'invalidCredentials' | 'error' | 'streaming' | 'registered'
426
- | 'attaching' | 'initializing'
427
-
428
- interface Camera {
429
- id: string
430
- name: string
431
- accountId: string
432
- bridgeId?: string | null
433
- locationId?: string | null
434
- status?: CameraStatus
435
- timezone?: string
436
- guid?: string
437
- ipAddress?: string
438
- macAddress?: string
439
- tags?: string[]
440
- notes?: string
441
- multiCameraId?: string
442
- recordingModes?: CameraRecordingModes
443
- deviceInfo?: CameraDeviceInfo
444
- shareDetails?: CameraShareDetails
445
- devicePosition?: CameraDevicePosition
446
- enabledAnalytics?: string[]
447
- packages?: string[]
448
- createdAt?: string
449
- updatedAt?: string
450
- }
451
-
452
- interface CameraDeviceInfo {
453
- make?: string // Manufacturer (e.g., "Axis", "Hikvision")
454
- model?: string // Model name
455
- firmwareVersion?: string
456
- directToCloud?: boolean // Direct-to-cloud camera (no bridge)
457
- serialNumber?: string
458
- resolution?: string
459
- type?: string // Camera type (e.g., "IP", "Analog")
460
- }
461
-
462
- interface CameraShareDetails {
463
- shared?: boolean
464
- accountId?: string // Sharing account ID
465
- firstResponder?: boolean
466
- permissions?: string[]
467
- }
468
-
469
- interface CameraDevicePosition {
470
- latitude?: number
471
- longitude?: number
472
- altitude?: number
473
- floor?: number
474
- azimuth?: number
475
- }
476
- ```
477
-
478
- ### User Parameter Types
479
-
480
- ```typescript
481
- interface ListUsersParams {
482
- pageSize?: number // Results per page (default: 100, max: 1000)
483
- pageToken?: string // Pagination token
484
- include?: string[] // Additional fields (e.g., ['permissions'])
485
- }
486
-
487
- interface GetUserParams {
488
- include?: string[] // Additional fields to include
489
- }
490
- ```
491
-
492
- ### Camera Parameter Types
493
-
494
- ```typescript
495
- interface ListCamerasParams {
496
- pageSize?: number // Results per page
497
- pageToken?: string // Pagination token
498
- include?: string[] // Additional fields to include
499
- sort?: string[] // Sort order
500
- status__in?: CameraStatus[] // Filter by status
501
- status__ne?: CameraStatus // Exclude by status
502
- tags__contains?: string[] // Filter by tags (all must match)
503
- tags__any?: string[] // Filter by tags (any match)
504
- name?: string // Exact name match
505
- name__contains?: string // Partial name match
506
- name__in?: string[] // Name in list
507
- id__in?: string[] // ID in list
508
- id__notIn?: string[] // ID not in list
509
- bridgeId__in?: string[] // Bridge ID filter
510
- locationId__in?: string[] // Location ID filter
511
- shared?: boolean // Shared camera filter
512
- directToCloud?: boolean // Direct-to-cloud filter
513
- q?: string // Full-text search
514
- // ... and more filter parameters
515
- }
516
-
517
- interface GetCameraParams {
518
- include?: string[] // Valid values: bridge, account, status, locationSummary,
519
- // deviceAddress, timeZone, notes, tags, devicePosition,
520
- // networkInfo, deviceInfo, effectivePermissions, firmware,
521
- // shareDetails, visibleByBridges, capabilities, analog,
522
- // packages, dewarpConfig, adminCredentials,
523
- // publicSafetySharing, enabledAnalytics
524
- }
525
- ```
526
-
527
- ### Bridge
528
-
529
- ```typescript
530
- type BridgeStatus =
531
- | 'online' | 'offline' | 'error' | 'idle'
532
- | 'registered' | 'attaching' | 'initializing'
533
-
534
- interface Bridge {
535
- id: string
536
- name: string
537
- accountId: string
538
- locationId?: string | null
539
- guid?: string
540
- timezone?: string
541
- status?: BridgeStatus | { connectionStatus?: BridgeStatus }
542
- tags?: string[]
543
- deviceInfo?: BridgeDeviceInfo
544
- networkInfo?: BridgeNetworkInfo
545
- devicePosition?: BridgeDevicePosition
546
- cameraCount?: number
547
- createdAt?: string
548
- updatedAt?: string
549
- }
550
-
551
- interface BridgeDeviceInfo {
552
- make?: string // Manufacturer
553
- model?: string // Model name
554
- firmwareVersion?: string
555
- serialNumber?: string
556
- hardwareVersion?: string
557
- }
558
-
559
- interface BridgeNetworkInfo {
560
- localIpAddress?: string
561
- publicIpAddress?: string
562
- macAddress?: string
563
- subnetMask?: string
564
- gateway?: string
565
- dnsServers?: string[]
566
- }
567
-
568
- interface BridgeDevicePosition {
569
- latitude?: number
570
- longitude?: number
571
- altitude?: number
572
- floor?: number
573
- azimuth?: number
574
- }
575
- ```
576
-
577
- ### Bridge Parameter Types
578
-
579
- ```typescript
580
- interface ListBridgesParams {
581
- pageSize?: number // Results per page
582
- pageToken?: string // Pagination token
583
- include?: string[] // Additional fields to include
584
- sort?: string[] // Sort order
585
- status__in?: BridgeStatus[] // Filter by status
586
- status__ne?: BridgeStatus // Exclude by status
587
- tags__contains?: string[] // Filter by tags (all must match)
588
- tags__any?: string[] // Filter by tags (any match)
589
- name?: string // Exact name match
590
- name__contains?: string // Partial name match
591
- name__in?: string[] // Name in list
592
- id__in?: string[] // ID in list
593
- id__notIn?: string[] // ID not in list
594
- locationId__in?: string[] // Location ID filter
595
- q?: string // Full-text search
596
- qRelevance__gte?: number // Minimum relevance score
597
- }
598
-
599
- interface GetBridgeParams {
600
- include?: string[] // Valid values: status, deviceInfo, networkInfo,
601
- // devicePosition, tags, effectivePermissions, etc.
602
- }
603
- ```
604
-
605
- ### Media Types
606
-
607
- ```typescript
608
- type MediaType = 'video' | 'image'
609
- type MediaStreamType = 'preview' | 'main'
610
-
611
- interface MediaInterval {
612
- type: MediaStreamType
613
- deviceId: string
614
- mediaType: MediaType
615
- startTimestamp: string // ISO 8601
616
- endTimestamp: string // ISO 8601
617
- flvUrl?: string | null
618
- rtspUrl?: string
619
- rtspsUrl?: string
620
- hlsUrl?: string
621
- mp4Url?: string
622
- multipartUrl?: string
623
- wsLiveUrl?: string
624
- }
625
-
626
- interface ListMediaParams {
627
- deviceId: string // Required - camera ID
628
- type: MediaStreamType // 'preview' or 'main'
629
- mediaType: MediaType // 'video' or 'image'
630
- startTimestamp: string // ISO 8601 start time
631
- endTimestamp?: string // ISO 8601 end time
632
- coalesce?: boolean // Merge adjacent intervals
633
- include?: string[] // e.g., ['flvUrl', 'hlsUrl', 'wsLiveUrl']
634
- pageToken?: string
635
- pageSize?: number
636
- }
637
-
638
- interface GetLiveImageParams {
639
- deviceId: string // Required - camera ID
640
- type?: 'preview' // Only 'preview' supported for live images
641
- }
642
-
643
- interface LiveImageResult {
644
- imageData: string // Base64 data URL (data:image/jpeg;base64,...)
645
- timestamp: string | null // X-Een-Timestamp header value
646
- prevToken: string | null // X-Een-PrevToken for navigation
647
- }
648
-
649
- interface GetRecordedImageParams {
650
- deviceId?: string // Camera ID (optional if using pageToken)
651
- pageToken?: string // Token for specific image
652
- type?: MediaStreamType // 'preview' or 'main'
653
- timestamp__lt?: string // Before timestamp
654
- timestamp__lte?: string // At or before timestamp
655
- timestamp?: string // Exact timestamp
656
- timestamp__gte?: string // At or after timestamp
657
- timestamp__gt?: string // After timestamp
658
- overlayId__in?: string[] // Overlay filter
659
- include?: string[] // e.g., ['overlayEmbedded', 'overlaySvgHeader']
660
- targetWidth?: number // Resize width
661
- targetHeight?: number // Resize height
662
- }
663
-
664
- interface RecordedImageResult {
665
- imageData: string // Base64 data URL
666
- timestamp: string | null // X-Een-Timestamp header value
667
- nextToken: string | null // X-Een-NextToken for next image
668
- prevToken: string | null // X-Een-PrevToken for previous image
669
- overlaySvg: string | null // X-Een-OverlaySvg for overlays
670
- }
671
-
672
- // Media Session types - for setting up cookie-based media access
673
- interface MediaSessionResponse {
674
- url: string // URL to call to set the session cookie
675
- }
676
-
677
- interface MediaSessionResult {
678
- success: boolean // Whether cookie was set successfully
679
- sessionUrl: string // The URL that was called
680
- }
681
- ```
682
-
683
- ### Feed Types
684
-
685
- ```typescript
686
- type FeedStreamType = 'main' | 'preview' | 'talkdown'
687
- type FeedMediaType = 'video' | 'audio' | 'image' | 'halfDuplex' | 'fullDuplex'
688
- type FeedIncludeOption = 'flvUrl' | 'rtspUrl' | 'rtspsUrl' | 'localRtspUrl' | 'hlsUrl' | 'multipartUrl' | 'webRtcUrl' | 'audioPushHttpsUrl'
689
-
690
- interface Feed {
691
- id: string // Feed identifier (typically deviceId-type)
692
- type: FeedStreamType // Stream type
693
- deviceId: string // Device generating this feed
694
- mediaType: FeedMediaType // Media type
695
- flvUrl?: string | null // Flash Video URL (if requested)
696
- rtspUrl?: string | null // RTSP URL (if requested)
697
- rtspsUrl?: string | null // RTSP over TLS (if requested)
698
- localRtspUrl?: string | null // Local RTSP to bridge (if requested)
699
- hlsUrl?: string | null // HLS URL (if requested)
700
- multipartUrl?: string | null // Multipart URL for raw frames (if requested)
701
- webRtcUrl?: string | null // WebRTC URL (if requested)
702
- audioPushHttpsUrl?: string | null // Audio push for speakers (if requested)
703
- }
704
-
705
- interface ListFeedsParams {
706
- deviceId?: string // Filter by single device ID
707
- deviceId__in?: string[] // Filter by multiple device IDs
708
- type?: FeedStreamType // Filter by stream type
709
- include?: FeedIncludeOption[] // URL fields to include in response
710
- pageSize?: number
711
- pageToken?: string
712
- }
713
-
714
- interface ListFeedsResult {
715
- results: Feed[]
716
- nextPageToken?: string
717
- totalSize?: number
718
- }
719
- ```
720
-
721
- ### Event Types
722
-
723
- ```typescript
724
- type ActorType = 'bridge' | 'camera' | 'speaker' | 'account' | 'user' | 'layout' | 'job' | 'measurement' | 'sensor' | 'gateway'
725
-
726
- interface Event {
727
- id: string
728
- startTimestamp: string // ISO 8601
729
- endTimestamp?: string | null
730
- span: boolean
731
- accountId: string
732
- actorId: string
733
- actorAccountId: string
734
- actorType: ActorType
735
- creatorId: string
736
- type: string // e.g., 'een.motionDetectionEvent.v1'
737
- dataSchemas: string[]
738
- data: EventData[]
739
- }
740
-
741
- interface EventData {
742
- type: string
743
- creatorId: string
744
- [key: string]: unknown // Event data is polymorphic
745
- }
746
-
747
- interface EventType {
748
- type: string // e.g., 'een.motionDetectionEvent.v1'
749
- name: string // Human-readable name
750
- description: string
751
- }
752
-
753
- interface EventFieldValues {
754
- type: string[] // Available event types for the actor
755
- }
756
-
757
- interface ListEventsParams {
758
- actor: string // Required: 'camera:{id}' format
759
- type__in: string[] // Required: event types to fetch
760
- startTimestamp__gte: string // Required: ISO 8601 timestamp
761
- endTimestamp__lte?: string // Optional: filter by end time
762
- pageSize?: number
763
- pageToken?: string
764
- sort?: '+startTimestamp' | '-startTimestamp'
765
- include?: string[] // e.g., ['data.een.fullFrameImageUrl.v1']
766
- }
767
-
768
- interface ListEventFieldValuesParams {
769
- actor: string // Required: 'camera:{id}' format
770
- }
771
- ```
772
-
773
- ### Event Metrics Types
774
-
775
- ```typescript
776
- type MetricActorType = 'bridge' | 'camera' | 'speaker' | 'account' | 'user' | 'layout' | 'job'
777
-
778
- // Data point: [timestamp_ms, value]
779
- type MetricDataPoint = [number, number]
780
-
781
- interface EventMetric {
782
- eventType: string
783
- actorId: string
784
- actorType: MetricActorType
785
- target: string // e.g., 'count'
786
- dataPoints: MetricDataPoint[]
787
- [key: string]: unknown // Additional properties
788
- }
789
-
790
- interface GetEventMetricsParams {
791
- actor: string // Required: 'camera:{id}' format
792
- eventType: string // Required: e.g., 'een.motionDetectionEvent.v1'
793
- timestamp__gte?: string // Optional: defaults to 7 days ago
794
- timestamp__lte?: string // Optional: defaults to now
795
- aggregateByMinutes?: number // Optional: default 60, minimum 60
796
- }
797
- ```
798
-
799
- ### Alert Types
800
-
801
- ```typescript
802
- interface AlertAction {
803
- name: string
804
- type: string
805
- success: boolean
806
- timestamp: string
807
- status?: 'fired' | 'success' | 'partialSuccess' | 'silenced' | 'failed' | 'internalError'
808
- }
809
-
810
- interface Alert {
811
- id: string
812
- timestamp: string
813
- createTimestamp: string
814
- creatorId: string
815
- alertType: string
816
- alertName?: string
817
- category?: string
818
- serviceRuleId?: string
819
- eventType?: string
820
- actorId: string
821
- actorType: string
822
- actorAccountId: string
823
- actorName?: string
824
- ruleId?: string
825
- eventId?: string
826
- locationId?: string
827
- locationName?: string
828
- priority?: number // 0-20
829
- dataSchemas?: string[]
830
- data?: Record<string, unknown>
831
- actions?: Record<string, AlertAction>
832
- description?: string
833
- }
834
-
835
- interface AlertType {
836
- type: string
837
- description: string
838
- }
839
-
840
- type AlertInclude = 'data' | 'actions' | 'dataSchemas' | 'description'
841
- type AlertSort = '+timestamp' | '-timestamp'
842
- type AlertActionStatus = 'fired' | 'success' | 'partialSuccess' | 'silenced' | 'failed' | 'internalError'
843
-
844
- interface ListAlertsParams {
845
- pageSize?: number
846
- pageToken?: string
847
- timestamp__lte?: string
848
- timestamp__gte?: string
849
- creatorId?: string
850
- alertType__in?: string[]
851
- actorId__in?: string[]
852
- actorType__in?: string[]
853
- actorAccountId?: string
854
- ruleId?: string
855
- ruleId__in?: string[]
856
- eventId?: string
857
- locationId__in?: string[]
858
- priority__gte?: number
859
- priority__lte?: number
860
- showInvalidAlerts?: boolean
861
- alertActionId__in?: string[]
862
- alertActionStatus__in?: AlertActionStatus[]
863
- include?: AlertInclude[]
864
- sort?: AlertSort[]
865
- language?: string
866
- }
867
-
868
- interface GetAlertParams {
869
- include?: AlertInclude[]
870
- }
871
-
872
- interface ListAlertTypesParams {
873
- pageSize?: number
874
- pageToken?: string
875
- }
876
- ```
877
-
878
- ### Notification Types
879
-
880
- ```typescript
881
- type NotificationCategory = 'health' | 'video' | 'operational' | 'audit' | 'job' | 'security' | 'sharing'
882
-
883
- type NotificationStatus =
884
- | 'pending' | 'bounced' | 'dropped' | 'deferred' | 'delivered' | 'sent'
885
- | 'outsideUsersSchedule' | 'notificationsDisabled' | 'noNotificationActions'
886
- | 'sendingFailed' | 'throttled' | 'unableToGetSettings'
887
-
888
- interface Notification {
889
- id: string
890
- timestamp: string
891
- createTimestamp: string
892
- sentTimestamp?: string
893
- alertId?: string | null
894
- alertType?: string
895
- actorId: string
896
- actorName?: string
897
- actorType: string
898
- actorAccountId: string
899
- userId: string
900
- accountId: string
901
- read: boolean
902
- status: NotificationStatus
903
- category: NotificationCategory
904
- description?: string
905
- notificationActions: string[]
906
- dataSchemas: string[]
907
- data: Record<string, unknown>
908
- }
909
-
910
- interface ListNotificationsParams {
911
- pageSize?: number
912
- pageToken?: string
913
- timestamp__lte?: string
914
- timestamp__gte?: string
915
- alertId?: string
916
- alertType?: string
917
- actorId?: string
918
- actorType?: string
919
- actorAccountId?: string
920
- category?: NotificationCategory
921
- userId?: string
922
- read?: boolean
923
- status?: NotificationStatus
924
- includeV1Notifications?: boolean
925
- sort?: ('+timestamp' | '-timestamp')[]
926
- language?: string
927
- }
928
- ```
929
-
930
210
  ---
931
211
 
932
- ## API Reference
933
-
934
- ### initEenToolkit
935
212
 
936
- Initialize the toolkit. Call this before using any API functions.
937
-
938
- ```typescript
939
- import { initEenToolkit } from 'een-api-toolkit'
940
-
941
- // In main.ts
942
- initEenToolkit({
943
- proxyUrl: import.meta.env.VITE_PROXY_URL,
944
- clientId: import.meta.env.VITE_EEN_CLIENT_ID,
945
- debug: true // optional
946
- })
947
- ```
948
-
949
- ### getAuthUrl
950
-
951
- Generate the OAuth authorization URL. Redirect the user here to start login.
952
-
953
- ```typescript
954
- import { getAuthUrl } from 'een-api-toolkit'
955
-
956
- function login() {
957
- window.location.href = getAuthUrl()
958
- }
959
- ```
960
-
961
- ### handleAuthCallback
962
-
963
- Handle the OAuth callback after user authorizes. Call this when user returns to your redirect URI.
964
-
965
- ```typescript
966
- import { handleAuthCallback } from 'een-api-toolkit'
967
-
968
- // In your callback route handler
969
- const url = new URL(window.location.href)
970
- const code = url.searchParams.get('code')
971
- const state = url.searchParams.get('state')
972
-
973
- if (code && state) {
974
- const { data, error } = await handleAuthCallback(code, state)
975
-
976
- if (error) {
977
- console.error('Auth failed:', error.message)
978
- return
979
- }
980
-
981
- // User is now authenticated
982
- router.push('/dashboard')
983
- }
984
- ```
985
-
986
- ### getCurrentUser
987
-
988
- Get the current authenticated user's profile.
989
-
990
- ```typescript
991
- import { getCurrentUser } from 'een-api-toolkit'
992
-
993
- const { data, error } = await getCurrentUser()
994
-
995
- if (error) {
996
- if (error.code === 'AUTH_REQUIRED') {
997
- router.push('/login')
998
- }
999
- return
1000
- }
1001
-
1002
- console.log(`Welcome, ${data.firstName} ${data.lastName}`)
1003
- ```
1004
-
1005
- ### getUsers
1006
-
1007
- List users with optional pagination.
1008
-
1009
- ```typescript
1010
- import { getUsers } from 'een-api-toolkit'
1011
-
1012
- // Basic usage
1013
- const { data, error } = await getUsers()
1014
-
1015
- // With pagination
1016
- const { data } = await getUsers({ pageSize: 50 })
1017
-
1018
- // Fetch all users
1019
- let allUsers: User[] = []
1020
- let pageToken: string | undefined
1021
-
1022
- do {
1023
- const { data, error } = await getUsers({ pageSize: 100, pageToken })
1024
- if (error) break
1025
- allUsers.push(...data.results)
1026
- pageToken = data.nextPageToken
1027
- } while (pageToken)
1028
- ```
1029
-
1030
- ### getUser
1031
-
1032
- Get a specific user by ID.
1033
-
1034
- ```typescript
1035
- import { getUser } from 'een-api-toolkit'
1036
-
1037
- const { data, error } = await getUser('user-id-123')
1038
-
1039
- if (error) {
1040
- if (error.code === 'NOT_FOUND') {
1041
- console.log('User not found')
1042
- }
1043
- return
1044
- }
1045
-
1046
- // With permissions
1047
- const { data: userWithPerms } = await getUser('user-id-123', {
1048
- include: ['permissions']
1049
- })
1050
- ```
1051
-
1052
- ### getCameras
1053
-
1054
- List cameras with optional pagination and filtering.
1055
-
1056
- ```typescript
1057
- import { getCameras } from 'een-api-toolkit'
1058
-
1059
- // Basic usage
1060
- const { data, error } = await getCameras()
1061
-
1062
- // With pagination
1063
- const { data } = await getCameras({ pageSize: 50 })
1064
-
1065
- // With status filter
1066
- const { data } = await getCameras({
1067
- pageSize: 20,
1068
- status__in: ['online', 'streaming']
1069
- })
1070
-
1071
- // With search
1072
- const { data } = await getCameras({
1073
- q: 'front door',
1074
- include: ['deviceInfo', 'status']
1075
- })
1076
- ```
1077
-
1078
- ### getCamera
1079
-
1080
- Get a specific camera by ID.
1081
-
1082
- ```typescript
1083
- import { getCamera } from 'een-api-toolkit'
1084
-
1085
- const { data, error } = await getCamera('camera-id-123')
1086
-
1087
- if (error) {
1088
- if (error.code === 'NOT_FOUND') {
1089
- console.log('Camera not found')
1090
- }
1091
- return
1092
- }
1093
-
1094
- // With additional fields
1095
- const { data: cameraWithDetails } = await getCamera('camera-id-123', {
1096
- include: ['deviceInfo', 'status', 'shareDetails']
1097
- })
1098
- ```
1099
-
1100
- ### getBridges
1101
-
1102
- List bridges with optional pagination and filtering.
1103
-
1104
- ```typescript
1105
- import { getBridges } from 'een-api-toolkit'
1106
-
1107
- // Basic usage
1108
- const { data, error } = await getBridges()
1109
-
1110
- // With pagination
1111
- const { data } = await getBridges({ pageSize: 50 })
1112
-
1113
- // With status filter
1114
- const { data } = await getBridges({
1115
- pageSize: 20,
1116
- status__in: ['online']
1117
- })
1118
-
1119
- // With search
1120
- const { data } = await getBridges({
1121
- q: 'office',
1122
- include: ['deviceInfo', 'status', 'networkInfo']
1123
- })
1124
- ```
1125
-
1126
- ### getBridge
1127
-
1128
- Get a specific bridge by ID.
1129
-
1130
- ```typescript
1131
- import { getBridge } from 'een-api-toolkit'
1132
-
1133
- const { data, error } = await getBridge('bridge-id-123')
1134
-
1135
- if (error) {
1136
- if (error.code === 'NOT_FOUND') {
1137
- console.log('Bridge not found')
1138
- }
1139
- return
1140
- }
1141
-
1142
- // With additional fields
1143
- const { data: bridgeWithDetails } = await getBridge('bridge-id-123', {
1144
- include: ['deviceInfo', 'networkInfo', 'status']
1145
- })
1146
- ```
1147
-
1148
- ### listMedia
1149
-
1150
- List media intervals for a camera. Useful for finding when recordings exist.
1151
-
1152
- ```typescript
1153
- import { listMedia } from 'een-api-toolkit'
1154
-
1155
- const { data, error } = await listMedia({
1156
- deviceId: 'camera-id-123',
1157
- type: 'preview',
1158
- mediaType: 'video',
1159
- startTimestamp: '2024-01-01T00:00:00.000Z',
1160
- endTimestamp: '2024-01-02T00:00:00.000Z'
1161
- })
1162
-
1163
- if (error) {
1164
- console.error('Failed to list media:', error.message)
1165
- return
1166
- }
1167
-
1168
- // Get available recording intervals
1169
- for (const interval of data.results) {
1170
- console.log(`Recording: ${interval.startTimestamp} - ${interval.endTimestamp}`)
1171
- }
1172
- ```
1173
-
1174
- ### getLiveImage
1175
-
1176
- Get a live preview image from a camera. Returns base64-encoded image data.
1177
-
1178
- ```typescript
1179
- import { getLiveImage } from 'een-api-toolkit'
1180
-
1181
- const { data, error } = await getLiveImage({
1182
- deviceId: 'camera-id-123'
1183
- })
1184
-
1185
- if (error) {
1186
- console.error('Failed to get live image:', error.message)
1187
- return
1188
- }
1189
-
1190
- // Use in an <img> element
1191
- const imgElement = document.querySelector('img')
1192
- imgElement.src = data.imageData // data:image/jpeg;base64,...
1193
-
1194
- // Timestamp of the image
1195
- console.log('Image timestamp:', data.timestamp)
1196
- ```
1197
-
1198
- ### getRecordedImage
1199
-
1200
- Get a recorded image from camera history. Supports timestamp-based navigation.
1201
-
1202
- ```typescript
1203
- import { getRecordedImage } from 'een-api-toolkit'
1204
-
1205
- // Get image at specific timestamp
1206
- const { data, error } = await getRecordedImage({
1207
- deviceId: 'camera-id-123',
1208
- timestamp: '2024-01-15T14:30:00.000Z'
1209
- })
1210
-
1211
- if (error) {
1212
- console.error('Failed to get recorded image:', error.message)
1213
- return
1214
- }
1215
-
1216
- // Display the image
1217
- const imgElement = document.querySelector('img')
1218
- if (imgElement) {
1219
- imgElement.src = data.imageData
1220
- }
1221
-
1222
- // Navigate to next/previous image
1223
- if (data.nextToken) {
1224
- const { data: nextImage } = await getRecordedImage({
1225
- pageToken: data.nextToken
1226
- })
1227
- // Use nextImage...
1228
- }
1229
- ```
1230
-
1231
- ### initMediaSession
1232
-
1233
- Initialize media session for cookie-based authentication. Required before using
1234
- multipart URLs directly in HTML elements.
1235
-
1236
- ```typescript
1237
- import { initMediaSession, listFeeds } from 'een-api-toolkit'
1238
-
1239
- // Initialize the media session (do this once after login)
1240
- const { data, error } = await initMediaSession()
1241
-
1242
- if (error) {
1243
- console.error('Failed to init media session:', error.message)
1244
- return
1245
- }
1246
-
1247
- console.log('Media session initialized:', data.sessionUrl)
1248
-
1249
- // Now multipart URLs can be used directly in <img> elements
1250
- const { data: feeds } = await listFeeds({
1251
- deviceId: 'camera-123',
1252
- include: ['multipartUrl']
1253
- })
1254
-
1255
- if (feeds?.results[0]?.multipartUrl) {
1256
- // This works because the session cookie is set
1257
- const imgElement = document.querySelector('img')
1258
- imgElement.src = feeds.results[0].multipartUrl
1259
- }
1260
- ```
1261
-
1262
- ### getMediaSession
1263
-
1264
- Get the media session URL without setting the cookie. Use `initMediaSession()`
1265
- for most cases.
1266
-
1267
- ```typescript
1268
- import { getMediaSession } from 'een-api-toolkit'
1269
-
1270
- // Get the session URL (step 1 of 2)
1271
- const { data, error } = await getMediaSession()
1272
-
1273
- if (error) {
1274
- console.error('Failed to get media session:', error.message)
1275
- return
1276
- }
1277
-
1278
- console.log('Session URL:', data.url)
1279
- // Manually call data.url with credentials: 'include' to set the cookie
1280
- ```
1281
-
1282
- ---
1283
-
1284
- ## Utilities
1285
-
1286
- ### formatTimestamp
1287
-
1288
- Converts ISO 8601 timestamps from `Z` (Zulu/UTC) format to `+00:00` format, as required by the EEN API.
1289
-
1290
- **Why this is needed:** JavaScript's `Date.toISOString()` returns timestamps with a `Z` suffix (e.g., `2025-01-15T22:30:00.000Z`), but the EEN API requires the `+00:00` format (e.g., `2025-01-15T22:30:00.000+00:00`).
1291
-
1292
- ```typescript
1293
- import { formatTimestamp } from 'een-api-toolkit'
1294
-
1295
- // Convert Z format to +00:00 format
1296
- formatTimestamp('2025-01-15T22:30:00.000Z')
1297
- // Returns: '2025-01-15T22:30:00.000+00:00'
1298
-
1299
- // Already in +00:00 format - returns unchanged
1300
- formatTimestamp('2025-01-15T22:30:00.000+00:00')
1301
- // Returns: '2025-01-15T22:30:00.000+00:00'
1302
-
1303
- // Common usage with Date objects
1304
- const now = new Date()
1305
- const apiTimestamp = formatTimestamp(now.toISOString())
1306
- // Returns timestamp in EEN API format
1307
- ```
1308
-
1309
- **When to use:**
1310
- - When displaying timestamps that match the exact format used in API calls (e.g., for debugging)
1311
- - When making direct API calls outside of the toolkit
1312
- - When building custom timestamp display components
1313
-
1314
- **Note:** The toolkit's API functions (`listAlerts`, `getEventMetrics`, `listNotifications`, `listEvents`, etc.) automatically apply `formatTimestamp` internally, so you don't need to pre-format timestamps when using these functions.
1315
-
1316
- ---
1317
-
1318
- ## Live Video Streaming
1319
-
1320
- The EEN API Toolkit supports two methods for displaying live video from cameras:
1321
-
1322
- ### Stream Types Comparison
1323
-
1324
- | Feature | Preview Stream | Main Stream |
1325
- |---------|---------------|-------------|
1326
- | Quality | Lower resolution | Full resolution |
1327
- | Authentication | Session cookie (multipart URL) | JWT token (Live SDK) |
1328
- | Element Type | `<img>` element | `<video>` element |
1329
- | Technology | MJPEG multipart | WebCodecs via SDK |
1330
- | Browser Support | All modern browsers | Chrome 94+, Edge 94+, Opera 80+ (WebCodecs) |
1331
- | Use Case | Thumbnails, quick previews | Full video playback |
1332
- | Setup | `initMediaSession()` | `@een/live-video-web-sdk` |
1333
-
1334
- ### Preview Streams (Multipart URL)
1335
-
1336
- Preview streams use cookie-based authentication with multipart URLs. These are ideal
1337
- for thumbnails and quick previews using simple `<img>` elements.
1338
-
1339
- **Step 1: Initialize the media session (once after login)**
1340
-
1341
- ```typescript
1342
- import { initMediaSession } from 'een-api-toolkit'
1343
-
1344
- // Call once after authentication
1345
- const { data, error } = await initMediaSession()
1346
- if (error) {
1347
- console.error('Failed to init media session:', error.message)
1348
- return
1349
- }
1350
- // Session cookie is now set
1351
- ```
1352
-
1353
- **Step 2: Get the feed URL and display it**
1354
-
1355
- ```typescript
1356
- import { listFeeds } from 'een-api-toolkit'
1357
-
1358
- const { data: feeds } = await listFeeds({
1359
- deviceId: cameraId,
1360
- type: 'preview',
1361
- include: ['multipartUrl']
1362
- })
1363
-
1364
- // Find a feed with multipartUrl
1365
- const previewFeed = feeds?.results.find(f => f.multipartUrl)
1366
- if (previewFeed?.multipartUrl) {
1367
- // Can be used directly in an <img> element
1368
- imgElement.src = previewFeed.multipartUrl
1369
- }
1370
- ```
1371
-
1372
- **Complete Vue Example (Preview Stream):**
1373
-
1374
- ```vue
1375
- <script setup lang="ts">
1376
- import { ref, onMounted } from 'vue'
1377
- import { initMediaSession, listFeeds, type Feed } from 'een-api-toolkit'
1378
-
1379
- const props = defineProps<{ cameraId: string }>()
1380
- const previewUrl = ref<string | null>(null)
1381
- const loading = ref(true)
1382
-
1383
- onMounted(async () => {
1384
- // Initialize media session for cookie-based auth
1385
- const { error: sessionError } = await initMediaSession()
1386
- if (sessionError) {
1387
- console.error('Failed to init session:', sessionError.message)
1388
- loading.value = false
1389
- return
1390
- }
1391
-
1392
- // Get preview feed
1393
- const { data, error } = await listFeeds({
1394
- deviceId: props.cameraId,
1395
- type: 'preview',
1396
- include: ['multipartUrl']
1397
- })
1398
-
1399
- if (error) {
1400
- console.error('Failed to get feeds:', error.message)
1401
- loading.value = false
1402
- return
1403
- }
1404
-
1405
- const feed = data.results.find(f => f.multipartUrl)
1406
- previewUrl.value = feed?.multipartUrl ?? null
1407
- loading.value = false
1408
- })
1409
- </script>
1410
-
1411
- <template>
1412
- <div class="preview-container">
1413
- <div v-if="loading">Loading...</div>
1414
- <img v-else-if="previewUrl" :src="previewUrl" alt="Camera preview" />
1415
- <div v-else>No preview available</div>
1416
- </div>
1417
- </template>
1418
- ```
1419
-
1420
- ### Main Streams (Live Video SDK)
1421
-
1422
- Main streams provide full-resolution video using the `@een/live-video-web-sdk`.
1423
- This requires JWT authentication and uses WebCodecs for efficient video playback.
1424
-
1425
- **Installation:**
1426
-
1427
- ```bash
1428
- npm install @een/live-video-web-sdk
1429
- ```
1430
-
1431
- **Complete Vue Example (Main Stream):**
1432
-
1433
- ```vue
1434
- <script setup lang="ts">
1435
- import { ref, onMounted, onUnmounted, nextTick } from 'vue'
1436
- import { LivePlayer } from '@een/live-video-web-sdk'
1437
- import { useAuthStore, listFeeds, type Feed } from 'een-api-toolkit'
1438
-
1439
- const props = defineProps<{ cameraId: string }>()
1440
- const authStore = useAuthStore()
1441
- const videoElement = ref<HTMLVideoElement | null>(null)
1442
- const loading = ref(true)
1443
- const statusMessage = ref('')
1444
-
1445
- let livePlayer: LivePlayer | null = null
1446
-
1447
- async function initPlayer() {
1448
- // Get the main feed to verify it exists
1449
- const { data, error } = await listFeeds({
1450
- deviceId: props.cameraId,
1451
- type: 'main',
1452
- include: ['multipartUrl']
1453
- })
1454
-
1455
- if (error || !data.results.length) {
1456
- statusMessage.value = 'No main feed available'
1457
- loading.value = false
1458
- return
1459
- }
1460
-
1461
- // Wait for video element to be mounted
1462
- await nextTick()
1463
- if (!videoElement.value) return
1464
-
1465
- // Ensure auth is valid
1466
- if (!authStore.baseUrl || !authStore.token) {
1467
- statusMessage.value = 'Not authenticated'
1468
- loading.value = false
1469
- return
1470
- }
1471
-
1472
- // Initialize the Live SDK player
1473
- livePlayer = new LivePlayer()
1474
-
1475
- // Subscribe to status updates
1476
- livePlayer.onStatusChange((status) => {
1477
- statusMessage.value = status
1478
- if (status === 'playing') {
1479
- loading.value = false
1480
- }
1481
- })
1482
-
1483
- // Start playback
1484
- await livePlayer.start({
1485
- videoElement: videoElement.value,
1486
- cameraId: props.cameraId,
1487
- baseUrl: authStore.baseUrl,
1488
- jwt: authStore.token
1489
- })
1490
- }
1491
-
1492
- function handleVideoError(event: Event) {
1493
- const video = event.target as HTMLVideoElement
1494
- console.error('Video error:', video.error?.message)
1495
- statusMessage.value = 'Playback error'
1496
- loading.value = false
1497
- }
1498
-
1499
- function cleanup() {
1500
- if (livePlayer) {
1501
- livePlayer.stop()
1502
- livePlayer = null
1503
- }
1504
- }
1505
-
1506
- onMounted(() => {
1507
- initPlayer()
1508
- })
1509
-
1510
- onUnmounted(() => {
1511
- cleanup()
1512
- })
1513
- </script>
1514
-
1515
- <template>
1516
- <div class="video-container">
1517
- <div v-if="loading" class="loading-overlay">
1518
- <span>{{ statusMessage || 'Connecting...' }}</span>
1519
- </div>
1520
- <video
1521
- ref="videoElement"
1522
- autoplay
1523
- muted
1524
- playsinline
1525
- @error="handleVideoError"
1526
- />
1527
- <div v-if="statusMessage && !loading" class="status">{{ statusMessage }}</div>
1528
- </div>
1529
- </template>
1530
-
1531
- <style scoped>
1532
- .video-container {
1533
- position: relative;
1534
- background: #000;
1535
- }
1536
- video {
1537
- width: 100%;
1538
- height: auto;
1539
- }
1540
- .loading-overlay {
1541
- position: absolute;
1542
- inset: 0;
1543
- display: flex;
1544
- align-items: center;
1545
- justify-content: center;
1546
- background: rgba(0, 0, 0, 0.7);
1547
- color: white;
1548
- }
1549
- </style>
1550
- ```
1551
-
1552
- ### Choosing Between Preview and Main Streams
1553
-
1554
- - **Preview streams** are simpler to implement and work well for:
1555
- - Camera selection grids
1556
- - Thumbnail previews
1557
- - Lower bandwidth scenarios
1558
- - Simple `<img>` element integration
1559
-
1560
- - **Main streams** are better for:
1561
- - Full-screen video viewing
1562
- - High-quality playback
1563
- - Professional monitoring applications
1564
- - Integration with video controls
1565
-
1566
- ---
1567
-
1568
- ## HLS Video Playback Troubleshooting
1569
-
1570
- For detailed troubleshooting, see the [HLS Video Troubleshooting Guide](./guides/HLS-VIDEO-TROUBLESHOOTING.md).
1571
-
1572
- ### Overview
1573
-
1574
- HLS video playback from the EEN API requires:
1575
-
1576
- 1. **Initialize media session** - `initMediaSession()`
1577
- 2. **Find recording intervals** - `listMedia()` with `include: ['hlsUrl']`
1578
- 3. **Extract HLS URL** - From interval containing target timestamp
1579
- 4. **Configure HLS.js with auth** - Bearer token in Authorization header
1580
-
1581
- ### Key Requirements
1582
-
1583
- | Requirement | Details |
1584
- |-------------|---------|
1585
- | Feed Type | HLS only available for `main` feeds, not `preview` |
1586
- | Timestamp Format | Use `formatTimestamp()` to convert `Z` to `+00:00` |
1587
- | Authentication | HLS.js requires `xhr.setRequestHeader('Authorization', `Bearer ${token}`)` |
1588
- | Recording Coverage | Target timestamp must fall within a recording interval |
1589
-
1590
- ### Common Issues
1591
-
1592
- #### 401 Unauthorized
1593
-
1594
- **Cause:** Using `withCredentials: true` instead of Authorization header.
1595
-
1596
- ```typescript
1597
- // WRONG
1598
- const hls = new Hls({
1599
- xhrSetup: (xhr) => { xhr.withCredentials = true }
1600
- })
1601
-
1602
- // CORRECT
1603
- import { useAuthStore } from 'een-api-toolkit'
1604
- const authStore = useAuthStore()
1605
-
1606
- const hls = new Hls({
1607
- xhrSetup: (xhr) => {
1608
- xhr.setRequestHeader('Authorization', `Bearer ${authStore.token}`)
1609
- }
1610
- })
1611
- ```
1612
-
1613
- #### No Video Available for Timestamp
1614
-
1615
- **Cause:** Search range too narrow or using wrong feed type.
1616
-
1617
- ```typescript
1618
- import { listMedia, formatTimestamp } from 'een-api-toolkit'
1619
-
1620
- const targetTime = new Date(alertTimestamp)
1621
- const searchStart = new Date(targetTime.getTime() - 60 * 60 * 1000) // 1 hour before
1622
- const searchEnd = new Date(targetTime.getTime() + 60 * 60 * 1000) // 1 hour after
1623
-
1624
- const result = await listMedia({
1625
- deviceId: cameraId,
1626
- type: 'main', // MUST be 'main' for HLS
1627
- mediaType: 'video',
1628
- startTimestamp: formatTimestamp(searchStart.toISOString()),
1629
- endTimestamp: formatTimestamp(searchEnd.toISOString()),
1630
- include: ['hlsUrl'] // MUST include 'hlsUrl'
1631
- })
1632
-
1633
- // Find interval containing target timestamp
1634
- const intervals = result.data?.results ?? []
1635
- const targetTimeMs = targetTime.getTime()
1636
-
1637
- const matchingInterval = intervals.find(i => {
1638
- if (!i.hlsUrl) return false
1639
- const start = new Date(i.startTimestamp).getTime()
1640
- const end = new Date(i.endTimestamp).getTime()
1641
- return targetTimeMs >= start && targetTimeMs <= end
1642
- })
1643
- ```
1644
-
1645
- #### Timestamp Format Error
1646
-
1647
- **Cause:** Using `Z` suffix instead of `+00:00`.
1648
-
1649
- ```typescript
1650
- // WRONG
1651
- const timestamp = new Date().toISOString() // "2025-01-15T22:30:00.000Z"
1652
-
1653
- // CORRECT
1654
- import { formatTimestamp } from 'een-api-toolkit'
1655
- const timestamp = formatTimestamp(new Date().toISOString()) // "2025-01-15T22:30:00.000+00:00"
1656
- ```
1657
-
1658
- ---
1659
213
 
1660
214
  ## Common Patterns
1661
215
 
1662
216
  ### Error Handling
1663
217
 
1664
218
  ```typescript
1665
- // All functions return Result<T>, never throw
1666
219
  const { data, error } = await getUsers()
1667
220
 
1668
221
  if (error) {
@@ -1670,12 +223,9 @@ if (error) {
1670
223
  case 'AUTH_REQUIRED':
1671
224
  router.push('/login')
1672
225
  break
1673
- case 'NETWORK_ERROR':
1674
- showRetryDialog()
226
+ case 'NOT_FOUND':
227
+ showNotFound()
1675
228
  break
1676
- case 'RATE_LIMITED':
1677
- await sleep(1000)
1678
- return retry()
1679
229
  default:
1680
230
  showError(error.message)
1681
231
  }
@@ -1689,75 +239,23 @@ processUsers(data.results)
1689
239
  ### Pagination
1690
240
 
1691
241
  ```typescript
1692
- // Manual pagination - fetch all
1693
- async function fetchAllUsers(): Promise<User[]> {
1694
- const allUsers: User[] = []
242
+ async function fetchAll<T>(
243
+ fetcher: (params: { pageToken?: string }) => Promise<Result<PaginatedResult<T>>>
244
+ ): Promise<T[]> {
245
+ const all: T[] = []
1695
246
  let pageToken: string | undefined
1696
247
 
1697
248
  do {
1698
- const { data, error } = await getUsers({ pageSize: 100, pageToken })
1699
- if (error) break // Stop on error, return what we have
1700
- allUsers.push(...data.results)
249
+ const { data, error } = await fetcher({ pageToken })
250
+ if (error) break
251
+ all.push(...data.results)
1701
252
  pageToken = data.nextPageToken
1702
253
  } while (pageToken)
1703
254
 
1704
- return allUsers
255
+ return all
1705
256
  }
1706
257
  ```
1707
258
 
1708
- ### Auth Guard (Vue Router)
1709
-
1710
- ```typescript
1711
- import { useAuthStore } from 'een-api-toolkit'
1712
-
1713
- router.beforeEach((to, from, next) => {
1714
- const authStore = useAuthStore()
1715
-
1716
- if (to.meta.requiresAuth && !authStore.isAuthenticated) {
1717
- next('/login')
1718
- } else {
1719
- next()
1720
- }
1721
- })
1722
- ```
1723
-
1724
- ### Vue Component Example
1725
-
1726
- ```vue
1727
- <script setup lang="ts">
1728
- import { ref, onMounted } from 'vue'
1729
- import { getCurrentUser, type UserProfile, type EenError } from 'een-api-toolkit'
1730
-
1731
- const user = ref<UserProfile | null>(null)
1732
- const loading = ref(false)
1733
- const error = ref<EenError | null>(null)
1734
-
1735
- async function fetchUser() {
1736
- loading.value = true
1737
- const result = await getCurrentUser()
1738
- loading.value = false
1739
-
1740
- if (result.error) {
1741
- error.value = result.error
1742
- return
1743
- }
1744
-
1745
- user.value = result.data
1746
- }
1747
-
1748
- onMounted(() => {
1749
- fetchUser()
1750
- })
1751
- </script>
1752
-
1753
- <template>
1754
- <div v-if="loading">Loading...</div>
1755
- <div v-else-if="error">{{ error.message }}</div>
1756
- <div v-else-if="user">Welcome, {{ user.firstName }}!</div>
1757
- <div v-else>Not authenticated or user data not available.</div>
1758
- </template>
1759
- ```
1760
-
1761
259
  ---
1762
260
 
1763
261
  ## Anti-Patterns (What NOT to Do)
@@ -1787,7 +285,7 @@ data.results.forEach(...) // TypeError if error occurred!
1787
285
  // CORRECT
1788
286
  const { data, error } = await getUsers()
1789
287
  if (error) return
1790
- data.results.forEach(...) // Safe - TypeScript knows data is not null
288
+ data.results.forEach(...) // Safe
1791
289
  ```
1792
290
 
1793
291
  ### DON'T: Call initEenToolkit multiple times
@@ -1801,28 +299,10 @@ export default {
1801
299
  }
1802
300
 
1803
301
  // CORRECT - call once in main.ts
1804
- // main.ts
1805
302
  initEenToolkit({ ... })
1806
303
  app.mount('#app')
1807
304
  ```
1808
305
 
1809
- ### DON'T: Access data before checking error
1810
-
1811
- ```typescript
1812
- // WRONG - unsafe access
1813
- const { data, error } = await getUser(id)
1814
- console.log(data.email) // TypeError if error!
1815
- if (error) { ... }
1816
-
1817
- // CORRECT - check error first
1818
- const { data, error } = await getUser(id)
1819
- if (error) {
1820
- console.error(error.message)
1821
- return
1822
- }
1823
- console.log(data.email) // Safe
1824
- ```
1825
-
1826
306
  ---
1827
307
 
1828
308
  ## External Resources
@@ -1830,4 +310,4 @@ console.log(data.email) // Safe
1830
310
  - [Eagle Eye Networks Developer Portal](https://developer.eagleeyenetworks.com)
1831
311
  - [EEN API v3.0 Reference](https://developer.eagleeyenetworks.com/reference/using-the-api)
1832
312
  - [GitHub Repository](https://github.com/klaushofrichter/een-api-toolkit)
1833
- - [OAuth Proxy](https://github.com/klaushofrichter/een-oauth-proxy) - Required for secure token management
313
+ - [OAuth Proxy](https://github.com/klaushofrichter/een-oauth-proxy)