een-api-toolkit 0.3.28 → 0.3.35

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 (68) hide show
  1. package/.claude/agents/docs-accuracy-reviewer.md +15 -3
  2. package/.claude/agents/een-auth-agent.md +131 -0
  3. package/.claude/agents/een-devices-agent.md +48 -13
  4. package/.claude/agents/een-events-agent.md +98 -0
  5. package/.claude/agents/een-grouping-agent.md +394 -0
  6. package/.claude/agents/een-media-agent.md +25 -5
  7. package/CHANGELOG.md +99 -10
  8. package/README.md +5 -3
  9. package/dist/index.cjs +3 -3
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +561 -0
  12. package/dist/index.js +388 -218
  13. package/dist/index.js.map +1 -1
  14. package/docs/AI-CONTEXT.md +13 -1
  15. package/docs/ai-reference/AI-AUTH.md +1 -1
  16. package/docs/ai-reference/AI-DEVICES.md +1 -1
  17. package/docs/ai-reference/AI-EVENTS.md +1 -1
  18. package/docs/ai-reference/AI-GROUPING.md +411 -0
  19. package/docs/ai-reference/AI-MEDIA.md +1 -1
  20. package/docs/ai-reference/AI-SETUP.md +1 -1
  21. package/docs/ai-reference/AI-USERS.md +1 -1
  22. package/examples/vue-alerts-metrics/README.md +2 -0
  23. package/examples/vue-alerts-metrics/alert-metrics-screenshot.png +0 -0
  24. package/examples/vue-alerts-metrics/e2e/auth.spec.ts +1 -1
  25. package/examples/vue-alerts-metrics/package-lock.json +17 -14
  26. package/examples/vue-alerts-metrics/package.json +1 -1
  27. package/examples/vue-bridges/package-lock.json +21 -15
  28. package/examples/vue-bridges/package.json +1 -1
  29. package/examples/vue-cameras/package-lock.json +21 -15
  30. package/examples/vue-cameras/package.json +1 -1
  31. package/examples/vue-event-subscriptions/README.md +2 -0
  32. package/examples/vue-event-subscriptions/event-subscriptions-screenshot.png +0 -0
  33. package/examples/vue-event-subscriptions/package-lock.json +17 -14
  34. package/examples/vue-event-subscriptions/package.json +1 -1
  35. package/examples/vue-events/events-screenshot.png +0 -0
  36. package/examples/vue-events/package-lock.json +17 -14
  37. package/examples/vue-events/package.json +1 -1
  38. package/examples/vue-feeds/package-lock.json +21 -15
  39. package/examples/vue-feeds/package.json +1 -1
  40. package/examples/vue-layouts/.env.example +12 -0
  41. package/examples/vue-layouts/README.md +320 -0
  42. package/examples/vue-layouts/e2e/app.spec.ts +76 -0
  43. package/examples/vue-layouts/e2e/auth.spec.ts +264 -0
  44. package/examples/vue-layouts/index.html +13 -0
  45. package/examples/vue-layouts/layouts-screenshot.png +0 -0
  46. package/examples/vue-layouts/package-lock.json +1722 -0
  47. package/examples/vue-layouts/package.json +28 -0
  48. package/examples/vue-layouts/playwright.config.ts +47 -0
  49. package/examples/vue-layouts/src/App.vue +124 -0
  50. package/examples/vue-layouts/src/components/LayoutModal.vue +456 -0
  51. package/examples/vue-layouts/src/main.ts +25 -0
  52. package/examples/vue-layouts/src/router/index.ts +62 -0
  53. package/examples/vue-layouts/src/views/Callback.vue +76 -0
  54. package/examples/vue-layouts/src/views/Home.vue +188 -0
  55. package/examples/vue-layouts/src/views/Layouts.vue +355 -0
  56. package/examples/vue-layouts/src/views/Login.vue +33 -0
  57. package/examples/vue-layouts/src/views/Logout.vue +59 -0
  58. package/examples/vue-layouts/src/vite-env.d.ts +12 -0
  59. package/examples/vue-layouts/tsconfig.json +21 -0
  60. package/examples/vue-layouts/tsconfig.node.json +10 -0
  61. package/examples/vue-layouts/vite.config.ts +12 -0
  62. package/examples/vue-media/media-screenshot.png +0 -0
  63. package/examples/vue-media/package-lock.json +19 -14
  64. package/examples/vue-media/package.json +1 -1
  65. package/examples/vue-users/package-lock.json +21 -16
  66. package/examples/vue-users/package.json +2 -2
  67. package/package.json +2 -2
  68. package/scripts/setup-agents.ts +0 -0
@@ -61,6 +61,8 @@ assistant: "I'll use the docs-accuracy-reviewer agent to verify the README and a
61
61
 
62
62
  1. **Discovery Phase**:
63
63
  - List all markdown files in the project (README.md, docs/**, CLAUDE.md, etc.)
64
+ - **Scan ALL example directories** (`examples/*/README.md`) - do not skip any
65
+ - Check agent files in `.claude/agents/*.md`
64
66
  - Identify the source code structure for cross-referencing
65
67
 
66
68
  2. **Analysis Phase**:
@@ -82,6 +84,15 @@ assistant: "I'll use the docs-accuracy-reviewer agent to verify the README and a
82
84
 
83
85
  ## Specific Checks to Perform
84
86
 
87
+ ### For Example Application Documentation:
88
+ - **Check ALL example apps** in `examples/*/README.md` (not just one)
89
+ - Verify screenshot references exist and filenames match actual files
90
+ - Confirm all listed API functions are exported from `src/index.ts`
91
+ - Check that `.env.example` files exist when referenced in setup instructions
92
+ - Validate project structure sections match actual directory contents
93
+ - Ensure port numbers are correct (should be `127.0.0.1:3333`)
94
+ - Verify code examples use current API signatures
95
+
85
96
  ### For API Documentation:
86
97
  - Compare documented function signatures with `src/index.ts` exports
87
98
  - Verify type definitions match `src/types/` directory
@@ -141,6 +152,7 @@ When reporting findings, use this structure:
141
152
 
142
153
  Before completing your review:
143
154
  1. Verify you've checked ALL markdown files in the project
144
- 2. Confirm each fix you made is backed by evidence from source code
145
- 3. Re-read modified sections to ensure they're clear and accurate
146
- 4. Check that your fixes didn't introduce new broken links or inconsistencies
155
+ 2. **Confirm ALL example app READMEs were reviewed** (list them in your report)
156
+ 3. Confirm each fix you made is backed by evidence from source code
157
+ 4. Re-read modified sections to ensure they're clear and accurate
158
+ 5. Check that your fixes didn't introduce new broken links or inconsistencies
@@ -123,10 +123,22 @@ authStore.isExpired // Computed: true if token expired
123
123
  ```
124
124
 
125
125
  ## Auth Guard Pattern
126
+
127
+ **CRITICAL**: The OAuth callback check MUST come BEFORE the auth check in the global guard.
128
+ The EEN IDP redirects to the root path (`/`) with `code` and `state` query parameters.
129
+ If you check authentication first, the user will be redirected to login before the callback is processed.
130
+
126
131
  ```typescript
127
132
  import { useAuthStore } from 'een-api-toolkit'
128
133
 
129
134
  router.beforeEach((to, from, next) => {
135
+ // IMPORTANT: Check for OAuth callback FIRST, before auth check
136
+ // EEN IDP redirects to root path with code and state params
137
+ if (to.path === '/' && to.query.code && to.query.state) {
138
+ next({ name: 'callback', query: to.query })
139
+ return
140
+ }
141
+
130
142
  const authStore = useAuthStore()
131
143
 
132
144
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
@@ -137,6 +149,10 @@ router.beforeEach((to, from, next) => {
137
149
  })
138
150
  ```
139
151
 
152
+ **WARNING**: Do NOT use route-specific `beforeEnter` guards for OAuth callback detection.
153
+ Global `beforeEach` guards run BEFORE route-specific guards, so the auth check will
154
+ block the callback before `beforeEnter` can redirect to the callback handler.
155
+
140
156
  ## Token Lifecycle
141
157
 
142
158
  1. **Login**: User redirects to EEN OAuth → Returns with code → Exchange for tokens
@@ -151,6 +167,38 @@ router.beforeEach((to, from, next) => {
151
167
  - **Session ID**: Client receives session ID to identify refresh session
152
168
  - **Token only**: Client stores only short-lived access token
153
169
 
170
+ ## Environment Variables
171
+
172
+ Required environment variables for OAuth:
173
+
174
+ ```
175
+ VITE_PROXY_URL=https://your-oauth-proxy.workers.dev # OAuth proxy URL
176
+ VITE_EEN_CLIENT_ID=YOUR-CLIENT-ID # EEN OAuth client ID
177
+ TEST_USER=user@example.com # For Playwright tests
178
+ TEST_PASSWORD=password # For Playwright tests
179
+ ```
180
+
181
+ ## localStorage Keys
182
+
183
+ The toolkit stores auth state in localStorage with these keys:
184
+
185
+ | Key | Description |
186
+ |-----|-------------|
187
+ | `een_token` | JWT access token |
188
+ | `een_tokenExpiration` | Token expiration timestamp (ms) |
189
+ | `een_sessionId` | Session ID for token refresh (proxy-side) |
190
+ | `een_hostname` | EEN API hostname (region-specific, e.g., `api.c021.eagleeyenetworks.com`) |
191
+ | `een_userProfile` | Cached user profile JSON |
192
+ | `een_refreshTokenMarker` | Indicates refresh token exists server-side (`"present"`) |
193
+
194
+ Useful for debugging:
195
+ ```typescript
196
+ // Check auth state in browser console
197
+ console.log('Token:', localStorage.getItem('een_token')?.substring(0, 50) + '...')
198
+ console.log('Expires:', new Date(parseInt(localStorage.getItem('een_tokenExpiration') || '0')))
199
+ console.log('Hostname:', localStorage.getItem('een_hostname'))
200
+ ```
201
+
154
202
  ## Constraints
155
203
  - Never expose refresh tokens to client code
156
204
  - Handle AUTH_REQUIRED errors by redirecting to login
@@ -158,6 +206,89 @@ router.beforeEach((to, from, next) => {
158
206
  - Always validate state parameter in callback
159
207
  - Clear auth state completely on logout
160
208
 
209
+ ## Vite Server Configuration
210
+
211
+ The Vite dev server MUST bind to `127.0.0.1` (not `localhost`) to match the redirect URI:
212
+
213
+ ```typescript
214
+ // vite.config.ts
215
+ export default defineConfig({
216
+ server: {
217
+ host: '127.0.0.1', // REQUIRED: Must match redirect URI
218
+ port: 3333,
219
+ strictPort: true
220
+ }
221
+ })
222
+ ```
223
+
224
+ ## EEN Login Page (Two-Step Process)
225
+
226
+ The EEN OAuth login page uses a **two-step authentication flow**:
227
+
228
+ 1. **Step 1 - Email**: User enters email address and clicks "Next"
229
+ 2. **Step 2 - Password**: Password field appears, user enters password and clicks "Sign in"
230
+
231
+ This is important for Playwright tests - you cannot fill both fields at once:
232
+
233
+ ```typescript
234
+ // Playwright test example for EEN two-step login
235
+ // Step 1: Enter email and click Next
236
+ const emailInput = page.locator('input[type="email"], input[type="text"]').first()
237
+ await emailInput.fill(TEST_USER)
238
+ await page.getByRole('button', { name: /next/i }).click()
239
+
240
+ // Step 2: Wait for password field and fill it
241
+ const passwordInput = page.locator('input[type="password"]')
242
+ await passwordInput.waitFor({ state: 'visible', timeout: 10000 })
243
+ await passwordInput.fill(TEST_PASSWORD)
244
+ await page.getByRole('button', { name: /sign in/i }).click()
245
+ ```
246
+
247
+ ## Playwright E2E Test Patterns
248
+
249
+ **Reference examples in:** `node_modules/een-api-toolkit/examples/*/e2e/auth.spec.ts`
250
+
251
+ Best practices for auth testing:
252
+ 1. **Fresh login per test**: Perform login for each test that needs auth (don't rely on state persistence)
253
+ 2. **Clear state after each test**: Use `afterEach` to clear localStorage/sessionStorage
254
+ 3. **Check proxy accessibility**: Skip OAuth tests if proxy is not reachable
255
+ 4. **Use EEN-specific selectors**: The EEN login page has specific IDs like `#authentication--input__email`
256
+
257
+ ```typescript
258
+ // Complete performLogin helper function
259
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
260
+ await page.goto('/login')
261
+ await page.click('button:has-text("Login with Eagle Eye Networks")')
262
+
263
+ // Wait for EEN OAuth page
264
+ await page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: 30000 })
265
+
266
+ // Step 1: Email
267
+ const emailInput = page.locator('#authentication--input__email, input[type="email"]').first()
268
+ await emailInput.waitFor({ state: 'visible', timeout: 15000 })
269
+ await emailInput.fill(username)
270
+ await page.getByRole('button', { name: 'Next' }).click()
271
+
272
+ // Step 2: Password
273
+ const passwordInput = page.locator('#authentication--input__password, input[type="password"]')
274
+ await passwordInput.waitFor({ state: 'visible', timeout: 10000 })
275
+ await passwordInput.fill(password)
276
+ await page.locator('#next, button:has-text("Sign in")').first().click()
277
+
278
+ // Wait for redirect back to app
279
+ await page.waitForURL(/127\.0\.0\.1:3333/, { timeout: 60000 })
280
+ await page.waitForURL('**/', { timeout: 60000 })
281
+ }
282
+
283
+ // Clear auth state helper
284
+ async function clearAuthState(page: Page): Promise<void> {
285
+ await page.evaluate(() => {
286
+ localStorage.clear()
287
+ sessionStorage.clear()
288
+ })
289
+ }
290
+ ```
291
+
161
292
  ## Common Errors
162
293
 
163
294
  | Error | Cause | Solution |
@@ -69,14 +69,14 @@ interface Camera {
69
69
  type CameraStatus =
70
70
  | 'online'
71
71
  | 'offline'
72
- | 'streaming'
73
- | 'recording'
74
- | 'registered'
75
72
  | 'deviceOffline'
76
73
  | 'bridgeOffline'
77
74
  | 'invalidCredentials'
78
75
  | 'error'
79
- | 'unknown'
76
+ | 'streaming'
77
+ | 'registered'
78
+ | 'attaching'
79
+ | 'initializing'
80
80
 
81
81
  // Status can also be nested in an object:
82
82
  // camera.status?.connectionStatus
@@ -97,7 +97,10 @@ type BridgeStatus =
97
97
  | 'online'
98
98
  | 'offline'
99
99
  | 'error'
100
- | 'unknown'
100
+ | 'idle'
101
+ | 'registered'
102
+ | 'attaching'
103
+ | 'initializing'
101
104
  ```
102
105
 
103
106
  ### ListCamerasParams
@@ -119,16 +122,29 @@ interface ListCamerasParams {
119
122
  ## Key Functions
120
123
 
121
124
  ### getCameras()
122
- List cameras with optional filters:
125
+ List cameras with optional filters.
126
+
127
+ **IMPORTANT:** The `status` field is NOT included by default. You must use `include: ['status']` to receive it:
128
+
123
129
  ```typescript
124
130
  import { getCameras, type Camera, type ListCamerasParams } from 'een-api-toolkit'
125
131
 
126
132
  const cameras = ref<Camera[]>([])
127
133
 
128
- // Get all online cameras
134
+ // Get all cameras WITH status - include: ['status'] is required!
135
+ async function fetchCameras() {
136
+ const result = await getCameras({
137
+ include: ['status'], // Required to get camera.status
138
+ pageSize: 100
139
+ })
140
+ // Now camera.status will be populated
141
+ }
142
+
143
+ // Get all online cameras (still need include for display)
129
144
  async function fetchOnlineCameras() {
130
145
  const result = await getCameras({
131
- status__in: ['online', 'streaming', 'recording'],
146
+ include: ['status'], // Required to display status in UI
147
+ status__in: ['online', 'streaming', 'registered'],
132
148
  pageSize: 100
133
149
  })
134
150
 
@@ -231,17 +247,33 @@ async function fetchBridge(bridgeId: string) {
231
247
 
232
248
  ```vue
233
249
  <script setup lang="ts">
234
- import { ref, onMounted } from 'vue'
235
- import { getCameras, type Camera, type ListCamerasParams } from 'een-api-toolkit'
250
+ import { ref, onMounted, computed } from 'vue'
251
+ import { getCameras, type Camera, type CameraStatus, type ListCamerasParams } from 'een-api-toolkit'
236
252
 
237
253
  const cameras = ref<Camera[]>([])
238
254
  const loading = ref(false)
239
- const statusFilter = ref<string[]>(['online', 'streaming', 'recording'])
255
+ const statusFilter = ref<string[]>(['online', 'streaming', 'registered'])
256
+
257
+ // Helper: status can be a string OR an object with connectionStatus
258
+ function getStatusString(status?: CameraStatus | { connectionStatus?: CameraStatus }): string | undefined {
259
+ if (!status) return undefined
260
+ if (typeof status === 'string') return status
261
+ return status.connectionStatus
262
+ }
263
+
264
+ // Computed property to pre-process cameras with status string (avoids calling helper multiple times in template)
265
+ const camerasWithStatus = computed(() =>
266
+ cameras.value.map(camera => ({
267
+ ...camera,
268
+ statusString: getStatusString(camera.status),
269
+ }))
270
+ )
240
271
 
241
272
  async function fetchCameras() {
242
273
  loading.value = true
243
274
 
244
275
  const params: ListCamerasParams = {
276
+ include: ['status'], // Required to receive status field
245
277
  pageSize: 100
246
278
  }
247
279
 
@@ -275,10 +307,13 @@ onMounted(fetchCameras)
275
307
 
276
308
  <div v-if="loading">Loading cameras...</div>
277
309
 
310
+ <!-- Use computed property for better performance (status string computed once per camera) -->
278
311
  <div class="camera-grid" v-else>
279
- <div v-for="camera in cameras" :key="camera.id" class="camera-card">
312
+ <div v-for="camera in camerasWithStatus" :key="camera.id" class="camera-card">
280
313
  <h3>{{ camera.name }}</h3>
281
- <span :class="camera.status">{{ camera.status }}</span>
314
+ <span :class="camera.statusString">
315
+ {{ camera.statusString || 'unknown' }}
316
+ </span>
282
317
  </div>
283
318
  </div>
284
319
  </div>
@@ -141,6 +141,77 @@ const actor = `camera:${cameraId}`
141
141
  const actor = `account:${accountId}`
142
142
  ```
143
143
 
144
+ ### listEventFieldValues()
145
+ Discover available event types for a specific camera:
146
+ ```typescript
147
+ import { listEventFieldValues } from 'een-api-toolkit'
148
+
149
+ async function getAvailableEventTypes(cameraId: string) {
150
+ const result = await listEventFieldValues({
151
+ actor: `camera:${cameraId}`
152
+ })
153
+
154
+ if (result.data) {
155
+ // result.data.type is an array of event type strings available for this camera
156
+ const availableTypes = result.data.type || []
157
+ // e.g., ['een.motionDetectionEvent.v1', 'een.tamperDetectionEvent.v1']
158
+ return availableTypes
159
+ }
160
+ return []
161
+ }
162
+ ```
163
+
164
+ ### listEventTypes()
165
+ Get human-readable names for event types:
166
+ ```typescript
167
+ import { listEventTypes } from 'een-api-toolkit'
168
+
169
+ async function fetchEventTypeNames() {
170
+ const result = await listEventTypes({ pageSize: 100 })
171
+
172
+ if (result.data) {
173
+ // Build a map of type -> name for display
174
+ const nameMap = new Map<string, string>()
175
+ for (const et of result.data.results) {
176
+ nameMap.set(et.type, et.name)
177
+ // e.g., 'een.motionDetectionEvent.v1' -> 'Motion Detection'
178
+ }
179
+ return nameMap
180
+ }
181
+ return new Map()
182
+ }
183
+
184
+ // Fallback: Parse event type string if API name not available
185
+ function parseEventTypeName(type: string): string {
186
+ const match = type.match(/een\.(\w+)Event\.v\d+/)
187
+ if (match) {
188
+ return match[1]
189
+ .replace(/([A-Z])/g, ' $1') // Add space before capitals
190
+ .replace(/^./, str => str.toUpperCase())
191
+ .trim()
192
+ }
193
+ return type
194
+ }
195
+ ```
196
+
197
+ ### Motion Detection Preselection Pattern
198
+ When implementing event type toggles, preselect motion detection by default:
199
+ ```typescript
200
+ const MOTION_DETECTION_EVENT = 'een.motionDetectionEvent.v1'
201
+
202
+ function preselectEventTypes(availableTypes: string[]): string[] {
203
+ // Preselect motion detection if available
204
+ if (availableTypes.includes(MOTION_DETECTION_EVENT)) {
205
+ return [MOTION_DETECTION_EVENT]
206
+ }
207
+ // Otherwise select the first available type
208
+ if (availableTypes.length > 0) {
209
+ return [availableTypes[0]]
210
+ }
211
+ return []
212
+ }
213
+ ```
214
+
144
215
  ### getEventMetrics()
145
216
  Get aggregated event counts:
146
217
  ```typescript
@@ -262,6 +333,33 @@ onUnmounted(async () => {
262
333
  })
263
334
  ```
264
335
 
336
+ ## Getting Event Thumbnails
337
+
338
+ Use `getRecordedImage()` to fetch a thumbnail image for an event:
339
+ ```typescript
340
+ import { getRecordedImage, type Event } from 'een-api-toolkit'
341
+
342
+ const eventImages = ref<Map<string, string>>(new Map())
343
+
344
+ async function fetchEventThumbnail(event: Event) {
345
+ // Extract camera ID from actor (format: "camera:{cameraId}")
346
+ const cameraId = event.actor.replace('camera:', '')
347
+
348
+ const result = await getRecordedImage({
349
+ cameraId,
350
+ timestamp: event.timestamp,
351
+ width: 120, // Thumbnail size
352
+ height: 80
353
+ })
354
+
355
+ if (result.data?.dataUrl) {
356
+ eventImages.value.set(event.id, result.data.dataUrl)
357
+ }
358
+ }
359
+
360
+ // In template: <img :src="eventImages.get(event.id)" />
361
+ ```
362
+
265
363
  ## Displaying Event Bounding Boxes
266
364
 
267
365
  Events can include SVG overlays showing where motion/objects were detected.