een-api-toolkit 0.3.30 → 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 +10 -7
  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 +101 -6
  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
@@ -141,7 +144,7 @@ async function fetchCameras() {
141
144
  async function fetchOnlineCameras() {
142
145
  const result = await getCameras({
143
146
  include: ['status'], // Required to display status in UI
144
- status__in: ['online', 'streaming', 'recording'],
147
+ status__in: ['online', 'streaming', 'registered'],
145
148
  pageSize: 100
146
149
  })
147
150
 
@@ -249,7 +252,7 @@ import { getCameras, type Camera, type CameraStatus, type ListCamerasParams } fr
249
252
 
250
253
  const cameras = ref<Camera[]>([])
251
254
  const loading = ref(false)
252
- const statusFilter = ref<string[]>(['online', 'streaming', 'recording'])
255
+ const statusFilter = ref<string[]>(['online', 'streaming', 'registered'])
253
256
 
254
257
  // Helper: status can be a string OR an object with connectionStatus
255
258
  function getStatusString(status?: CameraStatus | { connectionStatus?: CameraStatus }): string | undefined {
@@ -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.