een-api-toolkit 0.1.2 → 0.1.7

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 (47) hide show
  1. package/CHANGELOG.md +10 -53
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +686 -1
  5. package/dist/index.js +457 -222
  6. package/dist/index.js.map +1 -1
  7. package/docs/AI-CONTEXT.md +293 -1
  8. package/examples/vue-bridges/.env.example +13 -0
  9. package/examples/vue-bridges/e2e/app.spec.ts +73 -0
  10. package/examples/vue-bridges/e2e/auth.spec.ts +206 -0
  11. package/examples/vue-bridges/index.html +13 -0
  12. package/examples/vue-bridges/package-lock.json +1583 -0
  13. package/examples/vue-bridges/package.json +28 -0
  14. package/examples/vue-bridges/playwright.config.ts +46 -0
  15. package/examples/vue-bridges/src/App.vue +108 -0
  16. package/examples/vue-bridges/src/main.ts +23 -0
  17. package/examples/vue-bridges/src/router/index.ts +68 -0
  18. package/examples/vue-bridges/src/views/BridgeDetail.vue +279 -0
  19. package/examples/vue-bridges/src/views/Bridges.vue +297 -0
  20. package/examples/vue-bridges/src/views/Callback.vue +76 -0
  21. package/examples/vue-bridges/src/views/Home.vue +150 -0
  22. package/examples/vue-bridges/src/views/Login.vue +33 -0
  23. package/examples/vue-bridges/src/views/Logout.vue +66 -0
  24. package/examples/vue-bridges/src/vite-env.d.ts +12 -0
  25. package/examples/vue-bridges/tsconfig.json +21 -0
  26. package/examples/vue-bridges/tsconfig.node.json +10 -0
  27. package/examples/vue-bridges/vite.config.ts +12 -0
  28. package/examples/vue-media/.env.example +5 -0
  29. package/examples/vue-media/e2e/app.spec.ts +55 -0
  30. package/examples/vue-media/e2e/auth.spec.ts +344 -0
  31. package/examples/vue-media/index.html +13 -0
  32. package/examples/vue-media/package-lock.json +1583 -0
  33. package/examples/vue-media/package.json +28 -0
  34. package/examples/vue-media/playwright.config.ts +28 -0
  35. package/examples/vue-media/src/App.vue +122 -0
  36. package/examples/vue-media/src/main.ts +22 -0
  37. package/examples/vue-media/src/router/index.ts +61 -0
  38. package/examples/vue-media/src/views/Callback.vue +76 -0
  39. package/examples/vue-media/src/views/Home.vue +86 -0
  40. package/examples/vue-media/src/views/LiveCamera.vue +330 -0
  41. package/examples/vue-media/src/views/Login.vue +32 -0
  42. package/examples/vue-media/src/views/Logout.vue +59 -0
  43. package/examples/vue-media/src/vite-env.d.ts +12 -0
  44. package/examples/vue-media/tsconfig.json +21 -0
  45. package/examples/vue-media/tsconfig.node.json +10 -0
  46. package/examples/vue-media/vite.config.ts +12 -0
  47. package/package.json +1 -1
@@ -0,0 +1,55 @@
1
+ import { test, expect } from '@playwright/test'
2
+
3
+ test.describe('vue-media example app', () => {
4
+ test('home page shows login button when not authenticated', async ({ page }) => {
5
+ await page.goto('/')
6
+
7
+ // Should show the home page
8
+ await expect(page.getByRole('heading', { name: 'Welcome to the EEN Media Example' })).toBeVisible()
9
+
10
+ // Should show "not authenticated" state with login button
11
+ await expect(page.getByTestId('not-authenticated')).toBeVisible()
12
+ await expect(page.getByTestId('login-button')).toBeVisible()
13
+ await expect(page.getByText('Please log in to view live camera images')).toBeVisible()
14
+ })
15
+
16
+ test('login button navigates to login page', async ({ page }) => {
17
+ await page.goto('/')
18
+
19
+ await page.getByTestId('login-button').click()
20
+
21
+ await expect(page).toHaveURL('/login')
22
+ await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible()
23
+ await expect(page.getByText('Click the button below to authenticate with Eagle Eye Networks')).toBeVisible()
24
+ })
25
+
26
+ test('live route redirects to login when not authenticated', async ({ page }) => {
27
+ await page.goto('/live')
28
+
29
+ // Should redirect to login page (auth guard)
30
+ await expect(page).toHaveURL('/login')
31
+ })
32
+
33
+ test('navigation links work correctly', async ({ page }) => {
34
+ await page.goto('/')
35
+
36
+ // Check navigation is present
37
+ await expect(page.getByRole('navigation')).toBeVisible()
38
+
39
+ // Navigate to login via nav link
40
+ await page.getByRole('link', { name: 'Login' }).click()
41
+ await expect(page).toHaveURL('/login')
42
+
43
+ // Navigate back home
44
+ await page.getByRole('link', { name: 'Home' }).click()
45
+ await expect(page).toHaveURL('/')
46
+ })
47
+
48
+ test('about section displays toolkit function list', async ({ page }) => {
49
+ await page.goto('/')
50
+
51
+ // Check for the function descriptions
52
+ await expect(page.getByText('getCameras()')).toBeVisible()
53
+ await expect(page.getByText('getLiveImage()')).toBeVisible()
54
+ })
55
+ })
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Authenticated E2E tests for vue-media example
3
+ *
4
+ * These tests require:
5
+ * - TEST_USER and TEST_PASSWORD environment variables
6
+ * - VITE_EEN_CLIENT_ID environment variable
7
+ * - Running OAuth proxy server (see ../../../scripts/restart-proxy.sh)
8
+ */
9
+
10
+ import { test, expect, chromium } from '@playwright/test'
11
+ import * as fs from 'fs'
12
+ import * as path from 'path'
13
+ import { fileURLToPath } from 'url'
14
+ import dotenv from 'dotenv'
15
+
16
+ // Load environment variables from root .env
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
+ const rootDir = path.resolve(__dirname, '../../..')
19
+ dotenv.config({ path: path.join(rootDir, '.env') })
20
+
21
+ const PROXY_URL = process.env.VITE_PROXY_URL || 'http://localhost:8787'
22
+ const CLIENT_ID = process.env.VITE_EEN_CLIENT_ID
23
+ const REDIRECT_URI = 'http://127.0.0.1:3333'
24
+ const AUTH_CACHE_FILE = path.join(__dirname, '.auth-state.json')
25
+
26
+ interface AuthState {
27
+ token: string
28
+ tokenExpiration: number
29
+ baseUrl: string
30
+ sessionId: string
31
+ }
32
+
33
+ interface TokenResponse {
34
+ accessToken: string
35
+ expiresIn: number
36
+ httpsBaseUrl: string | { hostname: string; port?: number }
37
+ sessionId: string
38
+ }
39
+
40
+ /**
41
+ * Load cached auth state if valid
42
+ */
43
+ function loadCachedAuth(): AuthState | null {
44
+ if (!fs.existsSync(AUTH_CACHE_FILE)) {
45
+ return null
46
+ }
47
+ try {
48
+ const data = fs.readFileSync(AUTH_CACHE_FILE, 'utf-8')
49
+ const auth = JSON.parse(data) as AuthState
50
+ const bufferMs = 5 * 60 * 1000
51
+ if (Date.now() + bufferMs < auth.tokenExpiration) {
52
+ return auth
53
+ }
54
+ return null
55
+ } catch {
56
+ return null
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Perform OAuth login and return auth state
62
+ */
63
+ async function performLogin(): Promise<AuthState> {
64
+ const username = process.env.TEST_USER
65
+ const password = process.env.TEST_PASSWORD
66
+
67
+ if (!username || !password) {
68
+ throw new Error('TEST_USER and TEST_PASSWORD must be set in .env')
69
+ }
70
+
71
+ if (!CLIENT_ID) {
72
+ throw new Error('VITE_EEN_CLIENT_ID must be set in .env')
73
+ }
74
+
75
+ const browser = await chromium.launch({ headless: true })
76
+
77
+ try {
78
+ const context = await browser.newContext()
79
+ const page = await context.newPage()
80
+
81
+ const state = crypto.randomUUID()
82
+ let redirectUrl: string | null = null
83
+
84
+ page.on('request', (request) => {
85
+ const url = request.url()
86
+ if (url.includes('127.0.0.1:3333') && url.includes('code=')) {
87
+ redirectUrl = url
88
+ }
89
+ })
90
+
91
+ const authParams = new URLSearchParams({
92
+ client_id: CLIENT_ID,
93
+ response_type: 'code',
94
+ scope: 'vms.all',
95
+ redirect_uri: REDIRECT_URI,
96
+ state
97
+ })
98
+
99
+ await page.goto(`https://auth.eagleeyenetworks.com/oauth2/authorize?${authParams.toString()}`)
100
+ await page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: 15000 })
101
+
102
+ const emailInput = page.locator('#authentication--input__email')
103
+ await emailInput.waitFor({ state: 'visible', timeout: 15000 })
104
+ await emailInput.fill(username)
105
+
106
+ await page.getByRole('button', { name: 'Next' }).click()
107
+
108
+ const passwordInput = page.locator('#authentication--input__password')
109
+ await passwordInput.waitFor({ state: 'visible', timeout: 10000 })
110
+ await passwordInput.fill(password)
111
+
112
+ const signInButton = page.locator('#next')
113
+ try {
114
+ await signInButton.click()
115
+ } catch {
116
+ await page.getByRole('button', { name: 'Sign in' }).click()
117
+ }
118
+
119
+ try {
120
+ await page.waitForURL(/127\.0\.0\.1:3333.*code=/, { timeout: 30000 })
121
+ } catch {
122
+ if (!redirectUrl) {
123
+ const currentUrl = page.url()
124
+ if (currentUrl.includes('code=')) {
125
+ redirectUrl = currentUrl
126
+ }
127
+ }
128
+ }
129
+
130
+ if (!redirectUrl) {
131
+ throw new Error('Failed to capture redirect URL with authorization code')
132
+ }
133
+
134
+ const url = new URL(redirectUrl)
135
+ const code = url.searchParams.get('code')
136
+ const returnedState = url.searchParams.get('state')
137
+
138
+ if (!code || returnedState !== state) {
139
+ throw new Error('Invalid authorization response')
140
+ }
141
+
142
+ const tokenParams = new URLSearchParams({
143
+ code,
144
+ redirect_uri: REDIRECT_URI
145
+ })
146
+
147
+ const tokenResponse = await page.request.post(
148
+ `${PROXY_URL}/proxy/getAccessToken?${tokenParams.toString()}`,
149
+ {
150
+ headers: {
151
+ Accept: 'application/json',
152
+ Origin: 'http://127.0.0.1:3333'
153
+ }
154
+ }
155
+ )
156
+
157
+ if (!tokenResponse.ok()) {
158
+ throw new Error(`Token exchange failed: ${tokenResponse.status()}`)
159
+ }
160
+
161
+ const tokenData = (await tokenResponse.json()) as TokenResponse
162
+
163
+ let baseUrl: string
164
+ if (typeof tokenData.httpsBaseUrl === 'string') {
165
+ baseUrl = tokenData.httpsBaseUrl
166
+ } else {
167
+ const { hostname, port } = tokenData.httpsBaseUrl
168
+ baseUrl = port ? `https://${hostname}:${port}` : `https://${hostname}`
169
+ }
170
+
171
+ const authState: AuthState = {
172
+ token: tokenData.accessToken,
173
+ tokenExpiration: Date.now() + tokenData.expiresIn * 1000,
174
+ baseUrl,
175
+ sessionId: tokenData.sessionId
176
+ }
177
+
178
+ fs.writeFileSync(AUTH_CACHE_FILE, JSON.stringify(authState, null, 2), { mode: 0o600 })
179
+
180
+ return authState
181
+ } finally {
182
+ await browser.close()
183
+ }
184
+ }
185
+
186
+ test.describe('vue-media authenticated tests', () => {
187
+ let authState: AuthState
188
+
189
+ test.beforeAll(async () => {
190
+ // Get auth token (from cache or fresh login)
191
+ const cached = loadCachedAuth()
192
+ if (cached) {
193
+ console.log('Using cached auth token')
194
+ authState = cached
195
+ } else {
196
+ console.log('Performing OAuth login...')
197
+ authState = await performLogin()
198
+ console.log('Login successful')
199
+ }
200
+ })
201
+
202
+ test('authenticated user sees cameras on live page', async ({ page }) => {
203
+ // Set up localStorage with auth state before navigating
204
+ await page.goto('/')
205
+
206
+ // Inject auth state into the app's Pinia store via localStorage
207
+ await page.evaluate(
208
+ ({ token, baseUrl, sessionId, tokenExpiration }) => {
209
+ const authData = {
210
+ isAuthenticated: true,
211
+ token,
212
+ tokenExpiration,
213
+ refreshTokenMarker: 'present',
214
+ baseUrl,
215
+ sessionId
216
+ }
217
+ localStorage.setItem('een-auth', JSON.stringify(authData))
218
+ },
219
+ authState
220
+ )
221
+
222
+ // Navigate to live page
223
+ await page.goto('/live')
224
+
225
+ // Should show the live camera view
226
+ await expect(page.getByRole('heading', { name: 'Live Camera View' })).toBeVisible()
227
+
228
+ // Wait for cameras to load (or show no cameras message)
229
+ await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
230
+ timeout: 30000
231
+ })
232
+
233
+ // Either cameras are loaded or we see "no cameras" message
234
+ const cameraSelect = page.getByTestId('camera-select')
235
+ const noCameras = page.locator('.no-cameras')
236
+
237
+ const hasCameras = await cameraSelect.isVisible().catch(() => false)
238
+ const hasNoCameras = await noCameras.isVisible().catch(() => false)
239
+
240
+ expect(hasCameras || hasNoCameras).toBe(true)
241
+
242
+ if (hasCameras) {
243
+ console.log('Cameras found - checking controls')
244
+
245
+ // Verify controls are present
246
+ await expect(page.getByTestId('refresh-button')).toBeVisible()
247
+ await expect(page.getByTestId('auto-refresh-button')).toBeVisible()
248
+
249
+ // Wait for image to load
250
+ await page.waitForSelector('[data-testid="live-image"], .image-loading, .no-image', {
251
+ timeout: 30000
252
+ })
253
+
254
+ // Check if we got an image or an error
255
+ const hasImage = await page.getByTestId('live-image').isVisible().catch(() => false)
256
+ if (hasImage) {
257
+ console.log('Live image loaded successfully')
258
+
259
+ // Check timestamp is shown
260
+ const timestamp = page.getByTestId('timestamp')
261
+ await expect(timestamp).toBeVisible()
262
+ } else {
263
+ console.log('No live image available (camera may be offline)')
264
+ }
265
+ } else {
266
+ console.log('No cameras in account')
267
+ }
268
+ })
269
+
270
+ test('authenticated home page shows view live button', async ({ page }) => {
271
+ await page.goto('/')
272
+
273
+ // Inject auth state
274
+ await page.evaluate(
275
+ ({ token, baseUrl, sessionId, tokenExpiration }) => {
276
+ const authData = {
277
+ isAuthenticated: true,
278
+ token,
279
+ tokenExpiration,
280
+ refreshTokenMarker: 'present',
281
+ baseUrl,
282
+ sessionId
283
+ }
284
+ localStorage.setItem('een-auth', JSON.stringify(authData))
285
+ },
286
+ authState
287
+ )
288
+
289
+ // Reload to apply auth state
290
+ await page.reload()
291
+
292
+ // Should show authenticated state
293
+ await expect(page.getByTestId('authenticated')).toBeVisible()
294
+ await expect(page.getByTestId('view-live-button')).toBeVisible()
295
+ await expect(page.getByText('You are logged in!')).toBeVisible()
296
+ })
297
+
298
+ test('refresh button fetches new image', async ({ page }) => {
299
+ await page.goto('/')
300
+
301
+ // Inject auth state
302
+ await page.evaluate(
303
+ ({ token, baseUrl, sessionId, tokenExpiration }) => {
304
+ const authData = {
305
+ isAuthenticated: true,
306
+ token,
307
+ tokenExpiration,
308
+ refreshTokenMarker: 'present',
309
+ baseUrl,
310
+ sessionId
311
+ }
312
+ localStorage.setItem('een-auth', JSON.stringify(authData))
313
+ },
314
+ authState
315
+ )
316
+
317
+ await page.goto('/live')
318
+
319
+ // Wait for initial load
320
+ await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
321
+ timeout: 30000
322
+ })
323
+
324
+ const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
325
+
326
+ if (hasCameras) {
327
+ // Wait for initial image
328
+ await page.waitForSelector('[data-testid="live-image"], .no-image', {
329
+ timeout: 30000
330
+ })
331
+
332
+ // Click refresh
333
+ await page.getByTestId('refresh-button').click()
334
+
335
+ // Button should show loading state briefly
336
+ // Just verify the click works without errors
337
+ await expect(page.getByTestId('refresh-button')).toBeVisible()
338
+
339
+ console.log('Refresh button clicked successfully')
340
+ } else {
341
+ console.log('No cameras to test refresh with')
342
+ }
343
+ })
344
+ })
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>EEN Media Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>