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.
- package/.claude/agents/docs-accuracy-reviewer.md +15 -3
- package/.claude/agents/een-auth-agent.md +131 -0
- package/.claude/agents/een-devices-agent.md +10 -7
- package/.claude/agents/een-events-agent.md +98 -0
- package/.claude/agents/een-grouping-agent.md +394 -0
- package/.claude/agents/een-media-agent.md +25 -5
- package/CHANGELOG.md +101 -6
- package/README.md +5 -3
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +561 -0
- package/dist/index.js +388 -218
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +13 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +411 -0
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-alerts-metrics/README.md +2 -0
- package/examples/vue-alerts-metrics/alert-metrics-screenshot.png +0 -0
- package/examples/vue-alerts-metrics/e2e/auth.spec.ts +1 -1
- package/examples/vue-alerts-metrics/package-lock.json +17 -14
- package/examples/vue-alerts-metrics/package.json +1 -1
- package/examples/vue-bridges/package-lock.json +21 -15
- package/examples/vue-bridges/package.json +1 -1
- package/examples/vue-cameras/package-lock.json +21 -15
- package/examples/vue-cameras/package.json +1 -1
- package/examples/vue-event-subscriptions/README.md +2 -0
- package/examples/vue-event-subscriptions/event-subscriptions-screenshot.png +0 -0
- package/examples/vue-event-subscriptions/package-lock.json +17 -14
- package/examples/vue-event-subscriptions/package.json +1 -1
- package/examples/vue-events/events-screenshot.png +0 -0
- package/examples/vue-events/package-lock.json +17 -14
- package/examples/vue-events/package.json +1 -1
- package/examples/vue-feeds/package-lock.json +21 -15
- package/examples/vue-feeds/package.json +1 -1
- package/examples/vue-layouts/.env.example +12 -0
- package/examples/vue-layouts/README.md +320 -0
- package/examples/vue-layouts/e2e/app.spec.ts +76 -0
- package/examples/vue-layouts/e2e/auth.spec.ts +264 -0
- package/examples/vue-layouts/index.html +13 -0
- package/examples/vue-layouts/layouts-screenshot.png +0 -0
- package/examples/vue-layouts/package-lock.json +1722 -0
- package/examples/vue-layouts/package.json +28 -0
- package/examples/vue-layouts/playwright.config.ts +47 -0
- package/examples/vue-layouts/src/App.vue +124 -0
- package/examples/vue-layouts/src/components/LayoutModal.vue +456 -0
- package/examples/vue-layouts/src/main.ts +25 -0
- package/examples/vue-layouts/src/router/index.ts +62 -0
- package/examples/vue-layouts/src/views/Callback.vue +76 -0
- package/examples/vue-layouts/src/views/Home.vue +188 -0
- package/examples/vue-layouts/src/views/Layouts.vue +355 -0
- package/examples/vue-layouts/src/views/Login.vue +33 -0
- package/examples/vue-layouts/src/views/Logout.vue +59 -0
- package/examples/vue-layouts/src/vite-env.d.ts +12 -0
- package/examples/vue-layouts/tsconfig.json +21 -0
- package/examples/vue-layouts/tsconfig.node.json +10 -0
- package/examples/vue-layouts/vite.config.ts +12 -0
- package/examples/vue-media/media-screenshot.png +0 -0
- package/examples/vue-media/package-lock.json +19 -14
- package/examples/vue-media/package.json +1 -1
- package/examples/vue-users/package-lock.json +21 -16
- package/examples/vue-users/package.json +2 -2
- package/package.json +2 -2
- 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
|
|
145
|
-
3.
|
|
146
|
-
4.
|
|
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
|
-
| '
|
|
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
|
-
| '
|
|
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', '
|
|
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', '
|
|
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.
|