een-api-toolkit 0.3.15 → 0.3.20

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 (41) hide show
  1. package/CHANGELOG.md +45 -6
  2. package/README.md +1 -0
  3. package/dist/index.cjs +3 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +585 -0
  6. package/dist/index.js +485 -261
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +144 -1
  9. package/examples/vue-alerts-metrics/e2e/auth.spec.ts +8 -1
  10. package/examples/vue-alerts-metrics/package-lock.json +8 -1
  11. package/examples/vue-alerts-metrics/package.json +4 -3
  12. package/examples/vue-alerts-metrics/src/components/AlertsList.vue +567 -16
  13. package/examples/vue-alerts-metrics/src/components/CameraSelector.vue +16 -6
  14. package/examples/vue-alerts-metrics/src/components/MetricsChart.vue +23 -9
  15. package/examples/vue-alerts-metrics/src/components/NotificationsList.vue +579 -17
  16. package/examples/vue-alerts-metrics/src/components/TimeRangeSelector.vue +197 -12
  17. package/examples/vue-alerts-metrics/src/composables/useHlsPlayer.ts +285 -0
  18. package/examples/vue-alerts-metrics/src/views/Dashboard.vue +31 -9
  19. package/examples/vue-alerts-metrics/src/views/Home.vue +56 -7
  20. package/examples/vue-event-subscriptions/.env.example +15 -0
  21. package/examples/vue-event-subscriptions/README.md +103 -0
  22. package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
  23. package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
  24. package/examples/vue-event-subscriptions/index.html +13 -0
  25. package/examples/vue-event-subscriptions/package-lock.json +1719 -0
  26. package/examples/vue-event-subscriptions/package.json +28 -0
  27. package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
  28. package/examples/vue-event-subscriptions/src/App.vue +233 -0
  29. package/examples/vue-event-subscriptions/src/main.ts +25 -0
  30. package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
  31. package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
  32. package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
  33. package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +640 -0
  34. package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
  35. package/examples/vue-event-subscriptions/src/views/Logout.vue +59 -0
  36. package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +402 -0
  37. package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
  38. package/examples/vue-event-subscriptions/tsconfig.json +21 -0
  39. package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
  40. package/examples/vue-event-subscriptions/vite.config.ts +12 -0
  41. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { revokeToken } from 'een-api-toolkit'
5
+
6
+ const router = useRouter()
7
+ const processing = ref(true)
8
+ const error = ref<string | null>(null)
9
+
10
+ onMounted(async () => {
11
+ const result = await revokeToken()
12
+
13
+ if (result.error) {
14
+ // Even if revoke fails, the local state is cleared
15
+ console.warn('Token revocation failed:', result.error.message)
16
+ }
17
+
18
+ processing.value = false
19
+
20
+ // Redirect to home after a short delay
21
+ setTimeout(() => {
22
+ router.push('/')
23
+ }, 2000)
24
+ })
25
+ </script>
26
+
27
+ <template>
28
+ <div class="logout">
29
+ <div v-if="processing">
30
+ <h2>Logging out...</h2>
31
+ <p class="loading">Please wait.</p>
32
+ </div>
33
+
34
+ <div v-else>
35
+ <h2>Logged Out</h2>
36
+ <p>You have been successfully logged out.</p>
37
+ <p v-if="error" class="error">Note: {{ error }}</p>
38
+ <p class="redirect">Redirecting to home page...</p>
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <style scoped>
44
+ .logout {
45
+ text-align: center;
46
+ max-width: 400px;
47
+ margin: 0 auto;
48
+ }
49
+
50
+ h2 {
51
+ margin-bottom: 20px;
52
+ }
53
+
54
+ .redirect {
55
+ color: #666;
56
+ font-style: italic;
57
+ margin-top: 20px;
58
+ }
59
+ </style>
@@ -0,0 +1,402 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import {
4
+ listEventSubscriptions,
5
+ createEventSubscription,
6
+ deleteEventSubscription,
7
+ getCameras,
8
+ listEventTypes,
9
+ type EventSubscription,
10
+ type Camera,
11
+ type EventType,
12
+ type EenError,
13
+ type ListEventSubscriptionsParams
14
+ } from 'een-api-toolkit'
15
+
16
+ // Subscriptions state
17
+ const subscriptions = ref<EventSubscription[]>([])
18
+ const loading = ref(false)
19
+ const error = ref<EenError | null>(null)
20
+ const nextPageToken = ref<string | undefined>(undefined)
21
+ const hasNextPage = computed(() => !!nextPageToken.value)
22
+
23
+ // Create form state
24
+ const cameras = ref<Camera[]>([])
25
+ const eventTypes = ref<EventType[]>([])
26
+ const selectedCameras = ref<string[]>([])
27
+ const selectedEventTypes = ref<string[]>([])
28
+ const creating = ref(false)
29
+ const createError = ref<EenError | null>(null)
30
+ const createSuccess = ref(false)
31
+
32
+ // Delete state
33
+ const deleting = ref<string | null>(null)
34
+
35
+ const params = ref<ListEventSubscriptionsParams>({ pageSize: 10 })
36
+
37
+ async function fetchSubscriptions(fetchParams?: ListEventSubscriptionsParams, append = false) {
38
+ loading.value = true
39
+ error.value = null
40
+
41
+ const mergedParams = { ...params.value, ...fetchParams }
42
+ const result = await listEventSubscriptions(mergedParams)
43
+
44
+ if (result.error) {
45
+ error.value = result.error
46
+ if (!append) {
47
+ subscriptions.value = []
48
+ }
49
+ nextPageToken.value = undefined
50
+ } else {
51
+ if (append) {
52
+ subscriptions.value = [...subscriptions.value, ...result.data.results]
53
+ } else {
54
+ subscriptions.value = result.data.results
55
+ }
56
+ nextPageToken.value = result.data.nextPageToken
57
+ }
58
+
59
+ loading.value = false
60
+ return result
61
+ }
62
+
63
+ function refresh() {
64
+ return fetchSubscriptions()
65
+ }
66
+
67
+ async function fetchNextPage() {
68
+ if (!nextPageToken.value) return
69
+ return fetchSubscriptions({ ...params.value, pageToken: nextPageToken.value }, true)
70
+ }
71
+
72
+ /**
73
+ * Validates and formats an actor ID for event subscriptions.
74
+ * Actor IDs must follow the format "type:id" (e.g., "camera:abc123").
75
+ * @param type - The actor type (e.g., "camera", "bridge")
76
+ * @param id - The actor ID (alphanumeric, hyphens, underscores allowed)
77
+ * @returns The formatted actor ID or null if invalid
78
+ */
79
+ function formatActorId(type: string, id: string): string | null {
80
+ // Validate actor type (only allow known types)
81
+ const validTypes = ['camera', 'bridge', 'account', 'user']
82
+ if (!validTypes.includes(type)) {
83
+ return null
84
+ }
85
+ // Validate ID format (alphanumeric with hyphens and underscores)
86
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
87
+ return null
88
+ }
89
+ return `${type}:${id}`
90
+ }
91
+
92
+ async function loadFormData() {
93
+ // Load cameras
94
+ const camerasResult = await getCameras({ pageSize: 100 })
95
+ if (!camerasResult.error) {
96
+ cameras.value = camerasResult.data.results
97
+ }
98
+
99
+ // Load event types
100
+ const eventTypesResult = await listEventTypes({ pageSize: 100 })
101
+ if (!eventTypesResult.error) {
102
+ eventTypes.value = eventTypesResult.data.results
103
+ }
104
+ }
105
+
106
+ async function handleCreate() {
107
+ if (selectedCameras.value.length === 0 || selectedEventTypes.value.length === 0) {
108
+ createError.value = { code: 'VALIDATION_ERROR', message: 'Please select at least one camera and one event type' }
109
+ return
110
+ }
111
+
112
+ creating.value = true
113
+ createError.value = null
114
+ createSuccess.value = false
115
+
116
+ // Validate and format actor IDs
117
+ const actors: string[] = []
118
+ for (const id of selectedCameras.value) {
119
+ const actorId = formatActorId('camera', id)
120
+ if (!actorId) {
121
+ createError.value = { code: 'VALIDATION_ERROR', message: `Invalid camera ID format: ${id}` }
122
+ creating.value = false
123
+ return
124
+ }
125
+ actors.push(actorId)
126
+ }
127
+
128
+ const result = await createEventSubscription({
129
+ deliveryConfig: { type: 'serverSentEvents.v1' },
130
+ filters: [{
131
+ actors,
132
+ types: selectedEventTypes.value.map(type => ({ id: type }))
133
+ }]
134
+ })
135
+
136
+ if (result.error) {
137
+ createError.value = result.error
138
+ } else {
139
+ createSuccess.value = true
140
+ selectedCameras.value = []
141
+ selectedEventTypes.value = []
142
+ // Refresh the list
143
+ await fetchSubscriptions()
144
+ // Hide success message after 3 seconds
145
+ setTimeout(() => {
146
+ createSuccess.value = false
147
+ }, 3000)
148
+ }
149
+
150
+ creating.value = false
151
+ }
152
+
153
+ async function handleDelete(subscriptionId: string) {
154
+ if (!confirm('Are you sure you want to delete this subscription?')) {
155
+ return
156
+ }
157
+
158
+ deleting.value = subscriptionId
159
+ const result = await deleteEventSubscription(subscriptionId)
160
+
161
+ if (result.error) {
162
+ error.value = result.error
163
+ } else {
164
+ // Remove from local list
165
+ subscriptions.value = subscriptions.value.filter(s => s.id !== subscriptionId)
166
+ }
167
+
168
+ deleting.value = null
169
+ }
170
+
171
+ function getDeliveryType(sub: EventSubscription): string {
172
+ return sub.deliveryConfig.type
173
+ }
174
+
175
+ function getSseUrl(sub: EventSubscription): string | undefined {
176
+ if (sub.deliveryConfig.type === 'serverSentEvents.v1') {
177
+ return sub.deliveryConfig.sseUrl
178
+ }
179
+ return undefined
180
+ }
181
+
182
+ onMounted(async () => {
183
+ await Promise.all([
184
+ fetchSubscriptions(),
185
+ loadFormData()
186
+ ])
187
+ })
188
+ </script>
189
+
190
+ <template>
191
+ <div class="subscriptions">
192
+ <div class="header">
193
+ <h2>Event Subscriptions</h2>
194
+ <button @click="refresh" :disabled="loading">
195
+ {{ loading ? 'Loading...' : 'Refresh' }}
196
+ </button>
197
+ </div>
198
+
199
+ <!-- Create Form -->
200
+ <div class="create-form">
201
+ <h3>Create New Subscription</h3>
202
+
203
+ <div v-if="createError" class="error">
204
+ Error: {{ createError.message }}
205
+ </div>
206
+
207
+ <div v-if="createSuccess" class="success">
208
+ Subscription created successfully!
209
+ </div>
210
+
211
+ <div class="form-row">
212
+ <label>Select Cameras:</label>
213
+ <select v-model="selectedCameras" multiple data-testid="camera-select">
214
+ <option v-for="camera in cameras" :key="camera.id" :value="camera.id">
215
+ {{ camera.name || camera.id }}
216
+ </option>
217
+ </select>
218
+ <small>Hold Ctrl/Cmd to select multiple</small>
219
+ </div>
220
+
221
+ <div class="form-row">
222
+ <label>Select Event Types:</label>
223
+ <select v-model="selectedEventTypes" multiple data-testid="event-type-select">
224
+ <option v-for="eventType in eventTypes" :key="eventType.type" :value="eventType.type">
225
+ {{ eventType.name }} ({{ eventType.type }})
226
+ </option>
227
+ </select>
228
+ <small>Hold Ctrl/Cmd to select multiple</small>
229
+ </div>
230
+
231
+ <button
232
+ @click="handleCreate"
233
+ :disabled="creating || selectedCameras.length === 0 || selectedEventTypes.length === 0"
234
+ data-testid="create-subscription-button"
235
+ >
236
+ {{ creating ? 'Creating...' : 'Create Subscription' }}
237
+ </button>
238
+ </div>
239
+
240
+ <!-- Subscriptions List -->
241
+ <div class="list-section">
242
+ <h3>Active Subscriptions</h3>
243
+
244
+ <div v-if="loading && subscriptions.length === 0" class="loading">
245
+ Loading subscriptions...
246
+ </div>
247
+
248
+ <div v-else-if="error" class="error">
249
+ Error: {{ error.message }}
250
+ </div>
251
+
252
+ <div v-else>
253
+ <table v-if="subscriptions.length > 0" data-testid="subscriptions-table">
254
+ <thead>
255
+ <tr>
256
+ <th>ID</th>
257
+ <th>Type</th>
258
+ <th>Lifecycle</th>
259
+ <th>TTL</th>
260
+ <th>Actions</th>
261
+ </tr>
262
+ </thead>
263
+ <tbody>
264
+ <tr v-for="sub in subscriptions" :key="sub.id">
265
+ <td class="id-cell" :title="sub.id">{{ sub.id.slice(0, 12) }}...</td>
266
+ <td>{{ getDeliveryType(sub) }}</td>
267
+ <td>{{ sub.subscriptionConfig?.lifeCycle || '-' }}</td>
268
+ <td>{{ sub.subscriptionConfig?.timeToLiveSeconds ? `${sub.subscriptionConfig.timeToLiveSeconds}s` : '-' }}</td>
269
+ <td class="actions">
270
+ <router-link
271
+ v-if="getSseUrl(sub)"
272
+ :to="{ path: '/live', query: { subscriptionId: sub.id } }"
273
+ >
274
+ <button class="secondary small">Listen</button>
275
+ </router-link>
276
+ <button
277
+ class="danger small"
278
+ @click="handleDelete(sub.id)"
279
+ :disabled="deleting === sub.id"
280
+ :data-testid="`delete-${sub.id}`"
281
+ >
282
+ {{ deleting === sub.id ? 'Deleting...' : 'Delete' }}
283
+ </button>
284
+ </td>
285
+ </tr>
286
+ </tbody>
287
+ </table>
288
+
289
+ <p v-else class="no-data">No subscriptions found. Create one above to get started.</p>
290
+
291
+ <div v-if="hasNextPage" class="pagination">
292
+ <button @click="fetchNextPage" :disabled="loading">
293
+ {{ loading ? 'Loading...' : 'Load More' }}
294
+ </button>
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </template>
300
+
301
+ <style scoped>
302
+ .subscriptions {
303
+ max-width: 900px;
304
+ margin: 0 auto;
305
+ }
306
+
307
+ .header {
308
+ display: flex;
309
+ justify-content: space-between;
310
+ align-items: center;
311
+ margin-bottom: 20px;
312
+ }
313
+
314
+ .create-form {
315
+ background: #f8f9fa;
316
+ padding: 20px;
317
+ border-radius: 8px;
318
+ margin-bottom: 30px;
319
+ }
320
+
321
+ .create-form h3 {
322
+ margin-bottom: 15px;
323
+ }
324
+
325
+ .form-row {
326
+ margin-bottom: 15px;
327
+ }
328
+
329
+ .form-row label {
330
+ display: block;
331
+ margin-bottom: 5px;
332
+ font-weight: 500;
333
+ }
334
+
335
+ .form-row select {
336
+ width: 100%;
337
+ min-height: 100px;
338
+ padding: 8px;
339
+ border: 1px solid #ddd;
340
+ border-radius: 4px;
341
+ font-size: 14px;
342
+ }
343
+
344
+ .form-row small {
345
+ display: block;
346
+ margin-top: 5px;
347
+ color: #666;
348
+ font-size: 12px;
349
+ }
350
+
351
+ .list-section {
352
+ margin-top: 20px;
353
+ }
354
+
355
+ .list-section h3 {
356
+ margin-bottom: 15px;
357
+ }
358
+
359
+ table {
360
+ width: 100%;
361
+ border-collapse: collapse;
362
+ }
363
+
364
+ th,
365
+ td {
366
+ padding: 12px;
367
+ text-align: left;
368
+ border-bottom: 1px solid #eee;
369
+ }
370
+
371
+ th {
372
+ background: #f5f5f5;
373
+ font-weight: 600;
374
+ }
375
+
376
+ .id-cell {
377
+ font-family: monospace;
378
+ font-size: 12px;
379
+ }
380
+
381
+ .actions {
382
+ display: flex;
383
+ gap: 8px;
384
+ }
385
+
386
+ button.small {
387
+ padding: 6px 12px;
388
+ font-size: 12px;
389
+ }
390
+
391
+ .pagination {
392
+ margin-top: 20px;
393
+ text-align: center;
394
+ }
395
+
396
+ .no-data {
397
+ color: #666;
398
+ font-style: italic;
399
+ padding: 20px;
400
+ text-align: center;
401
+ }
402
+ </style>
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_PROXY_URL: string
5
+ readonly VITE_EEN_CLIENT_ID: string
6
+ readonly VITE_REDIRECT_URI: string
7
+ readonly VITE_DEBUG: string
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv
12
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "preserve",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
20
+ "references": [{ "path": "./tsconfig.node.json" }]
21
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ server: {
7
+ // IMPORTANT: Must use 127.0.0.1:3333 for EEN OAuth callback
8
+ // The EEN Identity Provider only permits this specific redirect URI
9
+ host: '127.0.0.1',
10
+ port: 3333
11
+ }
12
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "een-api-toolkit",
3
- "version": "0.3.15",
3
+ "version": "0.3.20",
4
4
  "description": "EEN Video platform API v3.0 library for Vue 3",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",