een-api-toolkit 0.3.79 → 0.3.81
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/een-auth-agent.md +19 -22
- package/.claude/agents/een-events-agent.md +64 -22
- package/.claude/agents/een-media-agent.md +56 -3
- package/.claude/agents/een-setup-agent.md +36 -85
- package/CHANGELOG.md +38 -32
- package/docs/AI-CONTEXT.md +1 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENT-DATA-SCHEMAS.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1 -1
- 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/package.json +1 -1
|
@@ -254,22 +254,24 @@ The EEN OAuth login page uses a **two-step authentication flow**:
|
|
|
254
254
|
1. **Step 1 - Email**: User enters email address and clicks "Next"
|
|
255
255
|
2. **Step 2 - Password**: Password field appears, user enters password and clicks "Sign in"
|
|
256
256
|
|
|
257
|
-
This is important for Playwright tests - you cannot fill both fields at once
|
|
257
|
+
This is important for Playwright tests - you cannot fill both fields at once.
|
|
258
|
+
|
|
259
|
+
**BEST PRACTICE:** Use `getByPlaceholder()` selectors — they are the most reliable across EEN login page versions:
|
|
258
260
|
|
|
259
261
|
```typescript
|
|
260
262
|
// Playwright test example for EEN two-step login
|
|
261
263
|
// Step 1: Enter email and click Next
|
|
262
|
-
|
|
263
|
-
await emailInput.fill(TEST_USER)
|
|
264
|
+
await page.getByPlaceholder(/email/i).fill(TEST_USER)
|
|
264
265
|
await page.getByRole('button', { name: /next/i }).click()
|
|
265
266
|
|
|
266
267
|
// Step 2: Wait for password field and fill it
|
|
267
|
-
|
|
268
|
-
await
|
|
269
|
-
await passwordInput.fill(TEST_PASSWORD)
|
|
270
|
-
await page.getByRole('button', { name: /sign in/i }).click()
|
|
268
|
+
await page.getByPlaceholder(/password/i).fill(TEST_PASSWORD, { timeout: 15000 })
|
|
269
|
+
await page.getByRole('button', { name: /sign in|next/i }).click()
|
|
271
270
|
```
|
|
272
271
|
|
|
272
|
+
**NOTE:** The `#authentication--input__email` and `#authentication--input__password` ID selectors
|
|
273
|
+
may not always be present. The `getByPlaceholder()` approach is more reliable.
|
|
274
|
+
|
|
273
275
|
## Playwright E2E Test Patterns
|
|
274
276
|
|
|
275
277
|
**Reference examples in:** `node_modules/een-api-toolkit/examples/*/e2e/auth.spec.ts`
|
|
@@ -283,27 +285,22 @@ Best practices for auth testing:
|
|
|
283
285
|
```typescript
|
|
284
286
|
// Complete performLogin helper function
|
|
285
287
|
async function performLogin(page: Page, username: string, password: string): Promise<void> {
|
|
286
|
-
await page.goto('/
|
|
287
|
-
await page.
|
|
288
|
+
await page.goto('/')
|
|
289
|
+
await page.getByRole('link', { name: 'Login' }).click()
|
|
288
290
|
|
|
289
291
|
// Wait for EEN OAuth page
|
|
290
|
-
await page.waitForURL(
|
|
292
|
+
await page.waitForURL(/auth\.eagleeyenetworks\.com|id\.eagleeyenetworks\.com/, { timeout: 15000 })
|
|
291
293
|
|
|
292
|
-
// Step 1: Email
|
|
293
|
-
|
|
294
|
-
await
|
|
295
|
-
await emailInput.fill(username)
|
|
296
|
-
await page.getByRole('button', { name: 'Next' }).click()
|
|
294
|
+
// Step 1: Email (use getByPlaceholder - most reliable selector)
|
|
295
|
+
await page.getByPlaceholder(/email/i).fill(username)
|
|
296
|
+
await page.getByRole('button', { name: /next/i }).click()
|
|
297
297
|
|
|
298
|
-
// Step 2: Password
|
|
299
|
-
|
|
300
|
-
await
|
|
301
|
-
await passwordInput.fill(password)
|
|
302
|
-
await page.locator('#next, button:has-text("Sign in")').first().click()
|
|
298
|
+
// Step 2: Password (appears after Next is clicked)
|
|
299
|
+
await page.getByPlaceholder(/password/i).fill(password, { timeout: 15000 })
|
|
300
|
+
await page.getByRole('button', { name: /sign in|next/i }).click()
|
|
303
301
|
|
|
304
302
|
// Wait for redirect back to app
|
|
305
|
-
await page.waitForURL(
|
|
306
|
-
await page.waitForURL('**/', { timeout: 60000 })
|
|
303
|
+
await page.waitForURL('http://127.0.0.1:3333/**', { timeout: 30000 })
|
|
307
304
|
}
|
|
308
305
|
|
|
309
306
|
// Clear auth state helper
|
|
@@ -381,22 +381,32 @@ async function fetchNotifications() {
|
|
|
381
381
|
4. **Cleanup** - Delete subscription when done (or it auto-expires after 15 min of inactivity)
|
|
382
382
|
|
|
383
383
|
### createEventSubscription()
|
|
384
|
+
|
|
385
|
+
**CRITICAL: The subscription API uses nested `deliveryConfig` and `filters` structures, NOT flat params.**
|
|
386
|
+
|
|
384
387
|
```typescript
|
|
388
|
+
import { ref, onUnmounted } from 'vue'
|
|
385
389
|
import {
|
|
386
390
|
createEventSubscription,
|
|
387
391
|
connectToEventSubscription,
|
|
388
392
|
deleteEventSubscription,
|
|
389
|
-
type
|
|
393
|
+
type SSEEvent,
|
|
394
|
+
type SSEConnection,
|
|
395
|
+
type SSEConnectionStatus
|
|
390
396
|
} from 'een-api-toolkit'
|
|
391
397
|
|
|
398
|
+
const sseConnection = ref<SSEConnection | null>(null)
|
|
392
399
|
const subscriptionId = ref<string | null>(null)
|
|
393
|
-
const sseConnection = ref<EventSource | null>(null)
|
|
394
400
|
|
|
395
|
-
async function startRealTimeEvents(cameraId: string) {
|
|
401
|
+
async function startRealTimeEvents(cameraId: string, eventTypes: string[]) {
|
|
396
402
|
// Step 1: Create subscription
|
|
403
|
+
// IMPORTANT: types must be objects with { id: string }, not plain strings
|
|
397
404
|
const result = await createEventSubscription({
|
|
398
|
-
|
|
399
|
-
|
|
405
|
+
deliveryConfig: { type: 'serverSentEvents.v1' },
|
|
406
|
+
filters: [{
|
|
407
|
+
actors: [`camera:${cameraId}`],
|
|
408
|
+
types: eventTypes.map(t => ({ id: t }))
|
|
409
|
+
}]
|
|
400
410
|
})
|
|
401
411
|
|
|
402
412
|
if (result.error) {
|
|
@@ -406,34 +416,54 @@ async function startRealTimeEvents(cameraId: string) {
|
|
|
406
416
|
|
|
407
417
|
subscriptionId.value = result.data.id
|
|
408
418
|
|
|
409
|
-
// Step 2:
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
419
|
+
// Step 2: Extract SSE URL from deliveryConfig (NOT from result.data.sseUrl)
|
|
420
|
+
const sseUrl = result.data.deliveryConfig.type === 'serverSentEvents.v1'
|
|
421
|
+
? result.data.deliveryConfig.sseUrl
|
|
422
|
+
: undefined
|
|
423
|
+
|
|
424
|
+
if (!sseUrl) return
|
|
425
|
+
|
|
426
|
+
// Step 3: Connect to SSE stream
|
|
427
|
+
// IMPORTANT: Returns a Result type { data, error }, NOT the connection directly
|
|
428
|
+
const connectionResult = connectToEventSubscription(sseUrl, {
|
|
429
|
+
onEvent: (event: SSEEvent) => {
|
|
430
|
+
console.log('Real-time event:', event.type, event.startTimestamp)
|
|
414
431
|
},
|
|
415
|
-
onError: (
|
|
416
|
-
console.error('SSE error:',
|
|
432
|
+
onError: (err: Error) => {
|
|
433
|
+
console.error('SSE error:', err.message)
|
|
417
434
|
},
|
|
418
|
-
|
|
419
|
-
|
|
435
|
+
onStatusChange: (status: SSEConnectionStatus) => {
|
|
436
|
+
// status: 'connected' | 'connecting' | 'disconnected' | 'error'
|
|
437
|
+
console.log('SSE status:', status)
|
|
420
438
|
}
|
|
421
439
|
})
|
|
422
440
|
|
|
423
|
-
|
|
441
|
+
if (connectionResult.error) {
|
|
442
|
+
console.error('Failed to connect:', connectionResult.error.message)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
sseConnection.value = connectionResult.data
|
|
424
447
|
}
|
|
425
448
|
|
|
426
|
-
// Cleanup when component unmounts
|
|
427
|
-
|
|
428
|
-
// Close SSE connection
|
|
449
|
+
// Cleanup when component unmounts or camera changes
|
|
450
|
+
async function cleanupSSE() {
|
|
429
451
|
if (sseConnection.value) {
|
|
430
452
|
sseConnection.value.close()
|
|
453
|
+
sseConnection.value = null
|
|
431
454
|
}
|
|
432
|
-
|
|
433
|
-
// Delete subscription
|
|
434
455
|
if (subscriptionId.value) {
|
|
435
|
-
|
|
456
|
+
try {
|
|
457
|
+
await deleteEventSubscription(subscriptionId.value)
|
|
458
|
+
} catch {
|
|
459
|
+
// Ignore cleanup errors
|
|
460
|
+
}
|
|
461
|
+
subscriptionId.value = null
|
|
436
462
|
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
onUnmounted(() => {
|
|
466
|
+
cleanupSSE()
|
|
437
467
|
})
|
|
438
468
|
```
|
|
439
469
|
|
|
@@ -568,9 +598,21 @@ async function createMetricsChart(canvas: HTMLCanvasElement, cameraId: string) {
|
|
|
568
598
|
| SUBSCRIPTION_LIMIT | Too many subscriptions | Delete old subscriptions |
|
|
569
599
|
| SSE_CONNECTION_FAILED | Can't connect to stream | Retry with backoff |
|
|
570
600
|
|
|
601
|
+
## Common SSE Mistakes
|
|
602
|
+
|
|
603
|
+
| Mistake | Correct Approach |
|
|
604
|
+
|---------|-----------------|
|
|
605
|
+
| `createEventSubscription({ actors, types })` flat params | Use `{ deliveryConfig: { type: 'serverSentEvents.v1' }, filters: [{ actors, types }] }` |
|
|
606
|
+
| `types: ['een.motionDetectionEvent.v1']` as strings | Use `types: [{ id: 'een.motionDetectionEvent.v1' }]` as objects |
|
|
607
|
+
| `result.data.sseUrl` for the SSE URL | Use `result.data.deliveryConfig.sseUrl` |
|
|
608
|
+
| Treating `connectToEventSubscription()` return as the connection | Returns `{ data: SSEConnection, error }` Result type |
|
|
609
|
+
| Using `onOpen` callback | Use `onStatusChange` with `SSEConnectionStatus` type |
|
|
610
|
+
| Forgetting to clean up when subscribed resource changes | Always clean up existing subscription before starting a new one |
|
|
611
|
+
|
|
571
612
|
## Constraints
|
|
572
613
|
- Always use actor format: `camera:{cameraId}` or `account:{accountId}`
|
|
573
|
-
- Always clean up SSE subscriptions on component unmount
|
|
614
|
+
- Always clean up SSE subscriptions on component unmount and when the subscribed resource changes
|
|
574
615
|
- Use formatTimestamp() for all timestamp parameters
|
|
575
616
|
- Include 'data.overlays' in include[] to get bounding box SVGs
|
|
576
617
|
- Handle SSE reconnection for long-running streams
|
|
618
|
+
- Use `listEventFieldValues()` to discover available event types before subscribing
|
|
@@ -67,13 +67,17 @@ assistant: "I'll use the een-media-agent to diagnose the HLS configuration and a
|
|
|
67
67
|
- Check authentication before media operations
|
|
68
68
|
- Pass config to LivePlayer's `start()` method, NOT the constructor
|
|
69
69
|
|
|
70
|
-
## Choosing the Right
|
|
70
|
+
## Choosing the Right Video Method
|
|
71
|
+
|
|
72
|
+
**IMPORTANT DECISION RULE:** When the user asks for "HD video", "main video", "full quality",
|
|
73
|
+
or "live video feed", you MUST use the **Live Video SDK** (`@een/live-video-web-sdk`).
|
|
74
|
+
Only use `multipartUrl` when explicitly asked for "preview", "thumbnail", or "low bandwidth" options.
|
|
71
75
|
|
|
72
76
|
| Use Case | Method | Why |
|
|
73
77
|
|----------|--------|-----|
|
|
74
78
|
| Grid of 20+ cameras | `getLiveImage()` | Lower bandwidth, manual refresh |
|
|
75
79
|
| Auto-updating preview | `multipartUrl` + `initMediaSession()` | Automatic updates, higher bandwidth |
|
|
76
|
-
| Full-quality live video | Live Video SDK | Full resolution, lowest latency |
|
|
80
|
+
| **Full-quality live video** | **Live Video SDK** | **Full resolution, lowest latency** |
|
|
77
81
|
| Recorded video playback | HLS via `listMedia()` | Seek capability, standard player |
|
|
78
82
|
|
|
79
83
|
**CRITICAL: Main feeds do NOT support multipartUrl**
|
|
@@ -83,7 +87,8 @@ The EEN API only returns `multipartUrl` for **preview feeds** (`type: 'preview'`
|
|
|
83
87
|
- **Preview feeds** → Use `multipartUrl` (MJPEG in `<img>` element)
|
|
84
88
|
- **Main feeds** → Use **Live Video SDK** (full HD in `<video>` element)
|
|
85
89
|
|
|
86
|
-
If you need HD quality video, you MUST use the Live Video SDK
|
|
90
|
+
If you need HD quality video, you MUST use the Live Video SDK (`npm install @een/live-video-web-sdk`).
|
|
91
|
+
Do not attempt to use `multipartUrl` with main feeds - it won't work.
|
|
87
92
|
|
|
88
93
|
## Key Functions
|
|
89
94
|
|
|
@@ -312,6 +317,52 @@ The video element MUST be rendered in the DOM before calling `player.start()`.
|
|
|
312
317
|
</style>
|
|
313
318
|
```
|
|
314
319
|
|
|
320
|
+
## CRITICAL: Camera Switching with LivePlayer
|
|
321
|
+
|
|
322
|
+
The LivePlayer SDK does **NOT cleanly release** the `<video>` element when `stop()` is called.
|
|
323
|
+
Reusing the same `<video>` element for a new LivePlayer instance will result in the video feed
|
|
324
|
+
**not updating** when the user switches cameras.
|
|
325
|
+
|
|
326
|
+
**Solution:** Use a Vue `:key` to force a fresh `<video>` element on each camera switch:
|
|
327
|
+
|
|
328
|
+
```vue
|
|
329
|
+
<script setup lang="ts">
|
|
330
|
+
const videoRef = ref<HTMLVideoElement | null>(null)
|
|
331
|
+
const videoKey = ref(0)
|
|
332
|
+
let livePlayer: LivePlayer | null = null
|
|
333
|
+
|
|
334
|
+
async function startStream(cameraId: string) {
|
|
335
|
+
// Stop previous player
|
|
336
|
+
if (livePlayer) {
|
|
337
|
+
livePlayer.stop()
|
|
338
|
+
livePlayer = null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// CRITICAL: Increment key to force Vue to create a new <video> element
|
|
342
|
+
videoKey.value++
|
|
343
|
+
await nextTick() // Wait for new element to be in DOM
|
|
344
|
+
|
|
345
|
+
livePlayer = new LivePlayer()
|
|
346
|
+
await livePlayer.start({
|
|
347
|
+
videoElement: videoRef.value!,
|
|
348
|
+
cameraId,
|
|
349
|
+
baseUrl: authStore.baseUrl ?? '',
|
|
350
|
+
jwt: authStore.token ?? ''
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
</script>
|
|
354
|
+
|
|
355
|
+
<template>
|
|
356
|
+
<!-- :key forces a fresh DOM element when videoKey changes -->
|
|
357
|
+
<video :key="videoKey" ref="videoRef" autoplay muted playsinline />
|
|
358
|
+
</template>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Why this is needed:** After `livePlayer.stop()`, the old `<video>` element retains stale
|
|
362
|
+
MediaSource/WebSocket state. Creating a new LivePlayer on the same element fails silently —
|
|
363
|
+
the video appears frozen on the previous camera's last frame. The `:key` trick makes Vue
|
|
364
|
+
destroy and recreate the DOM element, giving the new LivePlayer a clean slate.
|
|
365
|
+
|
|
315
366
|
## Error Handling
|
|
316
367
|
|
|
317
368
|
| Error Code | Meaning | Action |
|
|
@@ -332,3 +383,5 @@ The video element MUST be rendered in the DOM before calling `player.start()`.
|
|
|
332
383
|
| "Video Stream is done" immediately | Config passed to LivePlayer constructor | MUST use `new LivePlayer()` with no args, then `player.start(config)` |
|
|
333
384
|
| "Video element not found" | Video element not in DOM | Ensure video element is rendered (not hidden by v-if) before SDK init |
|
|
334
385
|
| Black video, no errors (LivePlayer) | Video element hidden by v-if | Use CSS visibility/opacity instead of v-if for conditional video display |
|
|
386
|
+
| Video doesn't change on camera switch | LivePlayer doesn't release video element cleanly | Use Vue `:key` on `<video>` element, increment on each switch (see Camera Switching section) |
|
|
387
|
+
| Used multipartUrl for "HD video" request | multipartUrl only supports preview feeds | Use Live Video SDK (`@een/live-video-web-sdk`) for HD/main feed video |
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
name: een-setup-agent
|
|
3
3
|
description: |
|
|
4
4
|
Use this agent when creating a new Vue 3 application with een-api-toolkit,
|
|
5
|
-
when fixing Pinia initialization errors, when
|
|
6
|
-
|
|
5
|
+
when fixing Pinia initialization errors, or when setting up Vite configuration
|
|
6
|
+
for EEN applications.
|
|
7
7
|
model: inherit
|
|
8
8
|
color: green
|
|
9
9
|
---
|
|
@@ -26,34 +26,37 @@ assistant: "I'll use the een-setup-agent to diagnose and fix the Pinia initializ
|
|
|
26
26
|
<Task tool call to launch een-setup-agent>
|
|
27
27
|
</example>
|
|
28
28
|
|
|
29
|
-
<example>
|
|
30
|
-
Context: User has OAuth redirect issues.
|
|
31
|
-
user: "My OAuth login redirects to the wrong URL"
|
|
32
|
-
assistant: "I'll use the een-setup-agent to check your vite.config.ts and redirect URI configuration."
|
|
33
|
-
<Task tool call to launch een-setup-agent>
|
|
34
|
-
</example>
|
|
35
|
-
|
|
36
29
|
## Context Files
|
|
37
30
|
Load these documentation files before starting:
|
|
38
31
|
- docs/AI-CONTEXT.md (overview)
|
|
39
32
|
- docs/ai-reference/AI-SETUP.md (primary reference)
|
|
40
33
|
|
|
34
|
+
## Scope and Agent Delegation
|
|
35
|
+
|
|
36
|
+
This agent handles **project scaffolding only**: Vite config, main.ts initialization,
|
|
37
|
+
basic router structure, placeholder views, and environment variables.
|
|
38
|
+
|
|
39
|
+
**After scaffolding, delegate to specialized agents for feature implementation:**
|
|
40
|
+
|
|
41
|
+
- **een-auth-agent** — OAuth login/logout views, auth callback handling, route guards,
|
|
42
|
+
session restoration (`authStore.initialize()`), Playwright E2E auth tests.
|
|
43
|
+
Knows the EEN two-step login flow and correct Playwright selectors.
|
|
44
|
+
- **een-media-agent** — Live video, camera previews, recorded images, HLS playback.
|
|
45
|
+
Knows when to use Live Video SDK vs multipartUrl and how to handle camera switching.
|
|
46
|
+
- **een-devices-agent** — Camera/bridge listing, filtering, and device details.
|
|
47
|
+
|
|
48
|
+
**Do NOT implement OAuth views (Login.vue, Callback.vue, Logout.vue) or media components
|
|
49
|
+
yourself.** Create placeholder views, then let the caller invoke the specialized agent.
|
|
50
|
+
|
|
41
51
|
## Your Capabilities
|
|
42
|
-
1. Create
|
|
43
|
-
2. Configure main.ts with proper Pinia + toolkit initialization
|
|
44
|
-
3. Set up vite.config.ts
|
|
45
|
-
4.
|
|
46
|
-
5. Set up environment variables
|
|
47
|
-
6. Debug
|
|
48
|
-
|
|
49
|
-
##
|
|
50
|
-
1. Verify prerequisites (Node 20+, Vue 3, Pinia)
|
|
51
|
-
2. Create or modify configuration files
|
|
52
|
-
3. Set up router with OAuth callback pattern
|
|
53
|
-
4. Verify setup by checking for common errors
|
|
54
|
-
5. Reference examples/vue-users/ for working patterns
|
|
55
|
-
|
|
56
|
-
## Key Configuration Points
|
|
52
|
+
1. Create Vue 3 + Vite + TypeScript project structure
|
|
53
|
+
2. Configure main.ts with proper Pinia + toolkit initialization order
|
|
54
|
+
3. Set up vite.config.ts (host: 127.0.0.1, port: 3333)
|
|
55
|
+
4. Create basic router with placeholder routes for /, /login, /callback, /logout
|
|
56
|
+
5. Set up .env environment variables
|
|
57
|
+
6. Debug Pinia initialization errors
|
|
58
|
+
|
|
59
|
+
## Key Configuration
|
|
57
60
|
|
|
58
61
|
### main.ts Initialization Order
|
|
59
62
|
```typescript
|
|
@@ -74,36 +77,7 @@ app.use(router)
|
|
|
74
77
|
app.mount('#app')
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
###
|
|
78
|
-
**Users will need to re-login after every page refresh unless you call `authStore.initialize()` in App.vue.**
|
|
79
|
-
|
|
80
|
-
```vue
|
|
81
|
-
<script setup lang="ts">
|
|
82
|
-
import { onMounted, computed } from 'vue'
|
|
83
|
-
import { useAuthStore } from 'een-api-toolkit'
|
|
84
|
-
|
|
85
|
-
const authStore = useAuthStore()
|
|
86
|
-
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
87
|
-
|
|
88
|
-
// CRITICAL: Initialize auth store from storage on app mount
|
|
89
|
-
// This restores the session if a valid token exists in localStorage/sessionStorage
|
|
90
|
-
onMounted(() => {
|
|
91
|
-
authStore.initialize()
|
|
92
|
-
})
|
|
93
|
-
</script>
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Without `initialize()`:
|
|
97
|
-
- Token is saved to localStorage after login ✓
|
|
98
|
-
- On page refresh, Pinia store starts with empty state
|
|
99
|
-
- `isAuthenticated` returns false → user must login again
|
|
100
|
-
|
|
101
|
-
With `initialize()`:
|
|
102
|
-
- Token is loaded from localStorage on app mount
|
|
103
|
-
- `isAuthenticated` returns true → session is restored
|
|
104
|
-
- User can continue without re-logging in
|
|
105
|
-
|
|
106
|
-
### vite.config.ts for EEN OAuth
|
|
80
|
+
### vite.config.ts
|
|
107
81
|
```typescript
|
|
108
82
|
import { defineConfig } from 'vite'
|
|
109
83
|
import vue from '@vitejs/plugin-vue'
|
|
@@ -111,48 +85,25 @@ import vue from '@vitejs/plugin-vue'
|
|
|
111
85
|
export default defineConfig({
|
|
112
86
|
plugins: [vue()],
|
|
113
87
|
server: {
|
|
114
|
-
|
|
115
|
-
// The EEN Identity Provider only permits this specific redirect URI
|
|
116
|
-
host: '127.0.0.1',
|
|
88
|
+
host: '127.0.0.1', // REQUIRED: Must match EEN OAuth redirect URI
|
|
117
89
|
port: 3333
|
|
118
90
|
}
|
|
119
91
|
})
|
|
120
92
|
```
|
|
121
93
|
|
|
122
|
-
### Router
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const routes = [
|
|
127
|
-
{ path: '/', component: () => import('@/views/Home.vue') },
|
|
128
|
-
{ path: '/login', component: () => import('@/views/Login.vue') },
|
|
129
|
-
{ path: '/callback', component: () => import('@/views/Callback.vue') },
|
|
130
|
-
{ path: '/logout', component: () => import('@/views/Logout.vue') }
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
const router = createRouter({
|
|
134
|
-
history: createWebHistory(),
|
|
135
|
-
routes
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
export default router
|
|
139
|
-
```
|
|
94
|
+
### Router Skeleton
|
|
95
|
+
Create routes for /, /login, /callback, /logout. Auth guards and OAuth callback
|
|
96
|
+
handling should be implemented by the **een-auth-agent**.
|
|
140
97
|
|
|
141
98
|
## Constraints
|
|
142
99
|
- Always use 127.0.0.1, never localhost
|
|
143
100
|
- Always use port 3333
|
|
144
101
|
- Pinia must be installed before initEenToolkit()
|
|
145
|
-
-
|
|
146
|
-
- Ensure VITE_PROXY_URL is set in .env file
|
|
147
|
-
- **Always call `authStore.initialize()` in App.vue onMounted** for session persistence
|
|
102
|
+
- Ensure VITE_PROXY_URL and VITE_EEN_CLIENT_ID are set in .env
|
|
148
103
|
|
|
149
|
-
## Common Errors
|
|
104
|
+
## Common Errors
|
|
150
105
|
|
|
151
106
|
| Error | Cause | Solution |
|
|
152
107
|
|-------|-------|----------|
|
|
153
|
-
| "Pinia not active" | initEenToolkit() called before
|
|
154
|
-
|
|
|
155
|
-
| "Invalid redirect_uri" | Trailing slash | Remove trailing slash from redirect URI |
|
|
156
|
-
| "CORS error" | Proxy not running | Start the OAuth proxy server |
|
|
157
|
-
| Session lost on refresh | Missing initialize() call | Add `authStore.initialize()` in App.vue onMounted |
|
|
158
|
-
| Must login after refresh | Missing initialize() call | Add `authStore.initialize()` in App.vue onMounted |
|
|
108
|
+
| "Pinia not active" | initEenToolkit() called before app.use(pinia) | Reorder initialization in main.ts |
|
|
109
|
+
| Auth/OAuth errors | See **een-auth-agent** | Delegate to een-auth-agent |
|
package/CHANGELOG.md
CHANGED
|
@@ -2,37 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.3.
|
|
5
|
+
## [0.3.81] - 2026-02-14
|
|
6
6
|
|
|
7
7
|
### Release Summary
|
|
8
8
|
|
|
9
|
-
#### PR #108: Release v0.3.76: Hostname validation security hardening
|
|
10
|
-
## Summary
|
|
11
|
-
- **Security Fix**: Validate hostname against EEN domain allowlist to prevent token exfiltration via malicious base URL injection
|
|
12
|
-
- **Hardening**: Fail-secure on tampered storage - clears all auth data when poisoned hostname/port detected
|
|
13
|
-
- **Validation**: Port validation (1-65535 range), protocol bypass prevention, subdomain spoofing protection
|
|
14
|
-
- **Tests**: Comprehensive hostname validation test suite for auth store (46 new tests)
|
|
15
|
-
- **Robustness**: Added `isAllowedEenHostname` utility with falsy guard, console.warn for rejected hostnames, `@internal` JSDoc tag on `ALLOWED_DOMAINS`
|
|
16
|
-
|
|
17
|
-
## Commits
|
|
18
|
-
- fix: Validate hostname against EEN domain allowlist to prevent token exfiltration
|
|
19
|
-
- test: Add hostname validation tests for auth store security fix
|
|
20
|
-
- docs: Add @internal JSDoc tag to ALLOWED_DOMAINS constant
|
|
21
|
-
- fix: Use console.warn for rejected hostname validation messages
|
|
22
|
-
- fix: Add falsy guard to isAllowedEenHostname for robustness
|
|
23
|
-
- fix: harden hostname/port validation and fail-secure on tampered storage
|
|
24
|
-
|
|
25
|
-
## Test Results
|
|
26
|
-
- **Lint**: Passed (1 warning - pre-existing)
|
|
27
|
-
- **Unit Tests**: 639 passed (23 test files)
|
|
28
|
-
- **Build**: Successful (ESM + CJS)
|
|
29
|
-
- **E2E Tests**: All 11 example apps passed
|
|
30
|
-
|
|
31
|
-
## Version
|
|
32
|
-
`0.3.76`
|
|
33
|
-
|
|
34
|
-
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
35
|
-
|
|
36
9
|
#### PR #110: fix: Security hardening - hostname validation and CI/CD pipeline
|
|
37
10
|
## Summary
|
|
38
11
|
|
|
@@ -69,18 +42,51 @@ Release v0.3.79 - Security hardening fixes for three vulnerabilities identified
|
|
|
69
42
|
|
|
70
43
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
71
44
|
|
|
45
|
+
#### PR #112: fix: revert broken head_branch check and update agents/skill
|
|
46
|
+
## Summary
|
|
47
|
+
|
|
48
|
+
Release v0.3.80 with two key changes:
|
|
49
|
+
|
|
50
|
+
1. **Fix broken publish workflow** — The `head_branch == 'production'` check added in v0.3.79 caused all automated npm publish runs to be skipped. GitHub's `workflow_run.head_branch` refers to the PR source branch (e.g. `develop`), not the target. The upstream `test-release.yml` already restricts to PRs merged to production.
|
|
51
|
+
|
|
52
|
+
2. **Add confidential data scan to PR-and-check skill** — New step that scans changed `.md` files for secrets, credentials, or confidential data before PR creation. Critical safeguard for a public npm package.
|
|
53
|
+
|
|
54
|
+
3. **Agent documentation improvements** — Updated Playwright selectors (getByPlaceholder), SSE API patterns (nested deliveryConfig/filters), LivePlayer camera switching (:key trick), and setup agent scope (delegation to specialized agents).
|
|
55
|
+
|
|
56
|
+
## Commits
|
|
57
|
+
|
|
58
|
+
- `1bb7297` fix: revert broken head_branch check in npm-publish workflow
|
|
59
|
+
- `6afd34a` chore: add confidential data scan to PR-and-check skill and update agents
|
|
60
|
+
- `c5ff7eb` Merge pull request #111
|
|
61
|
+
|
|
62
|
+
## Test Results
|
|
63
|
+
|
|
64
|
+
- **Lint**: 0 errors (1 pre-existing warning)
|
|
65
|
+
- **Unit tests**: 644 passed
|
|
66
|
+
- **Build**: Success
|
|
67
|
+
- **E2E tests**: 3/11 passed before flaky auth timeout in vue-cameras (camera-settings.spec.ts:328, unrelated to changes)
|
|
68
|
+
- **Security review**: No vulnerabilities found
|
|
69
|
+
- **Confidential data scan**: No secrets in 224 .md files
|
|
70
|
+
|
|
71
|
+
## Version
|
|
72
|
+
|
|
73
|
+
`0.3.80`
|
|
74
|
+
|
|
75
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
76
|
+
|
|
72
77
|
|
|
73
78
|
### Detailed Changes
|
|
74
79
|
|
|
75
80
|
#### Bug Fixes
|
|
76
|
-
- fix:
|
|
81
|
+
- fix: use Vue ref() pattern in een-events-agent SSE example
|
|
82
|
+
- fix: revert broken head_branch check in npm-publish workflow
|
|
77
83
|
|
|
78
84
|
#### Other Changes
|
|
79
|
-
-
|
|
85
|
+
- chore: add confidential data scan to PR-and-check skill and update agents
|
|
80
86
|
|
|
81
87
|
### Links
|
|
82
88
|
- [npm package](https://www.npmjs.com/package/een-api-toolkit)
|
|
83
|
-
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.
|
|
89
|
+
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.79...v0.3.81)
|
|
84
90
|
|
|
85
91
|
---
|
|
86
|
-
*Released: 2026-02-14
|
|
92
|
+
*Released: 2026-02-14 10:39:08 CST*
|
package/docs/AI-CONTEXT.md
CHANGED