velora-node-sdk 1.0.0
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/LICENSE +6 -0
- package/README.md +503 -0
- package/dist/auth/CredentialManager.d.ts +17 -0
- package/dist/auth/CredentialManager.js +116 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/client/VeloraClient.d.ts +32 -0
- package/dist/client/VeloraClient.js +124 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +5 -0
- package/dist/http/HttpClient.d.ts +22 -0
- package/dist/http/HttpClient.js +116 -0
- package/dist/http/RetryManager.d.ts +9 -0
- package/dist/http/RetryManager.js +43 -0
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +14 -0
- package/dist/types/api.d.ts +128 -0
- package/dist/types/api.js +2 -0
- package/dist/types/client.d.ts +44 -0
- package/dist/types/client.js +2 -0
- package/dist/types/errors.d.ts +30 -0
- package/dist/types/errors.js +105 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +19 -0
- package/dist/ws/MessageQueue.d.ts +8 -0
- package/dist/ws/MessageQueue.js +26 -0
- package/dist/ws/ReconnectManager.d.ts +13 -0
- package/dist/ws/ReconnectManager.js +42 -0
- package/dist/ws/WebSocketClient.d.ts +21 -0
- package/dist/ws/WebSocketClient.js +127 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +9 -0
- package/package.json +39 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# Velora SDK
|
|
2
|
+
|
|
3
|
+
Node.js SDK for the Velora local API, providing easy access to the Velora Electron app's playback controls, state queries, and real-time event streaming.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **HTTP & WebSocket Support** — Read current track, playback state, and playback queue; control playback (play, pause, skip, seek)
|
|
8
|
+
- **App Registration** — Secure request-based authorization with user consent
|
|
9
|
+
- **Token Management** — Automatic token polling and permission checking
|
|
10
|
+
- **Real-time Events** — Connect to WebSocket for track changes and playback state updates
|
|
11
|
+
- **Auto-reconnection** — Exponential backoff reconnection on WebSocket disconnect
|
|
12
|
+
- **TypeScript Ready** — Full type definitions for all APIs
|
|
13
|
+
- **Error Classification** — Specific error types for easier error handling
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install velora-node-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Register Your App
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { VeloraClient } from 'velora-node-sdk';
|
|
27
|
+
|
|
28
|
+
const client = new VeloraClient();
|
|
29
|
+
|
|
30
|
+
const registrationConfig = {
|
|
31
|
+
app: {
|
|
32
|
+
name: 'My awesome app',
|
|
33
|
+
description: 'An application that is awesome!',
|
|
34
|
+
developer: 'ItsMeDev',
|
|
35
|
+
website: 'https://example.com',
|
|
36
|
+
icon: 'https://example.com/icon.png',
|
|
37
|
+
},
|
|
38
|
+
permissions: ['read', 'write'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const requestId = await client.registerApp(registrationConfig);
|
|
42
|
+
console.log('Registration request ID:', requestId);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Poll for User Approval
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const maxWaitMs = 5 * 60 * 1000;
|
|
49
|
+
await client.requestAccessToken(requestId, maxWaitMs);
|
|
50
|
+
console.log('User approved! Token acquired.');
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Use HTTP Endpoints
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const health = await client.getHealth();
|
|
57
|
+
console.log('Velora is running on port:', health.port);
|
|
58
|
+
|
|
59
|
+
const track = await client.getCurrentTrack();
|
|
60
|
+
console.log(`Now playing: ${track.track.title} by ${track.track.artist}`);
|
|
61
|
+
|
|
62
|
+
const playbackState = await client.getPlaybackState();
|
|
63
|
+
console.log('Is playing:', playbackState.is_playing);
|
|
64
|
+
|
|
65
|
+
const queue = await client.getPlaybackQueue();
|
|
66
|
+
console.log('Queue size:', queue.queue.length);
|
|
67
|
+
|
|
68
|
+
const user = await client.getUserPublic();
|
|
69
|
+
console.log('User:', user.user.name);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. Control Playback
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
await client.play('OrZoMJcXje2');
|
|
76
|
+
await client.pause();
|
|
77
|
+
await client.next();
|
|
78
|
+
await client.seek(60000);
|
|
79
|
+
await client.toggle();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 5. Listen to Real-time Events
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
await client.connectWebSocket();
|
|
86
|
+
|
|
87
|
+
client.on('track_changed', (data) => {
|
|
88
|
+
console.log(`Track changed: ${data.title} by ${data.artist}`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
client.on('playback_state_changed', (data) => {
|
|
92
|
+
console.log('Playing:', data.is_playing);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
client.on('ws:connected', () => {
|
|
96
|
+
console.log('WebSocket connected');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
client.on('ws:disconnected', () => {
|
|
100
|
+
console.log('WebSocket disconnected');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
client.on('ws:error', (error) => {
|
|
104
|
+
console.error('WebSocket error:', error);
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API Reference
|
|
109
|
+
|
|
110
|
+
### VeloraClient
|
|
111
|
+
|
|
112
|
+
#### Constructor
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
new VeloraClient(config?: ClientConfig)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Options:**
|
|
119
|
+
|
|
120
|
+
- `host` (string, default: `127.0.0.1`) — Velora server host
|
|
121
|
+
- `port` (number, default: `39031`) — Velora server port
|
|
122
|
+
- `token` (string, optional) — Pre-existing access token
|
|
123
|
+
- `requestedPermissions` (array, optional) — Permissions for pre-existing token
|
|
124
|
+
- `httpOptions` (object, optional) — HTTP client options (timeout, maxRetries)
|
|
125
|
+
- `wsOptions` (object, optional) — WebSocket options
|
|
126
|
+
|
|
127
|
+
#### Authentication Methods
|
|
128
|
+
|
|
129
|
+
##### `registerApp(appConfig: RegisterAppRequest): Promise<string>`
|
|
130
|
+
|
|
131
|
+
Register your app and request permissions. Returns a `request_id` to poll.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const requestId = await client.registerApp({
|
|
135
|
+
app: {
|
|
136
|
+
name: 'My App',
|
|
137
|
+
description: 'Optional description',
|
|
138
|
+
developer: 'Developer name',
|
|
139
|
+
website: 'https://example.com',
|
|
140
|
+
icon: 'https://example.com/icon.png',
|
|
141
|
+
},
|
|
142
|
+
permissions: ['read', 'write'],
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
##### `requestAccessToken(requestId: string, maxWaitMs?: number): Promise<void>`
|
|
147
|
+
|
|
148
|
+
Poll for user approval. Waits up to `maxWaitMs` (default 5 minutes).
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
await client.requestAccessToken(requestId);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
##### `isAuthenticated(): boolean`
|
|
155
|
+
|
|
156
|
+
Check if client has a valid token.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
if (client.isAuthenticated()) {
|
|
160
|
+
console.log('Ready to use API');
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
##### `getToken(): string | null`
|
|
165
|
+
|
|
166
|
+
Get the current access token.
|
|
167
|
+
|
|
168
|
+
##### `setToken(token: string, permissions?: PermissionType[]): void`
|
|
169
|
+
|
|
170
|
+
Manually set a token (e.g., from storage).
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
client.setToken('velora_...', ['read', 'write']);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
##### `clearCredentials(): void`
|
|
177
|
+
|
|
178
|
+
Clear token and permissions.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
client.clearCredentials();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Read Endpoints
|
|
185
|
+
|
|
186
|
+
All read endpoints require `read` permission.
|
|
187
|
+
|
|
188
|
+
##### `getHealth(): Promise<HealthResponse>`
|
|
189
|
+
|
|
190
|
+
Check Velora server status.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const health = await client.getHealth();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
##### `getCurrentTrack(): Promise<CurrentTrackResponse>`
|
|
197
|
+
|
|
198
|
+
Get the current playing track.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const response = await client.getCurrentTrack();
|
|
202
|
+
console.log(response.track.title);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
##### `getPlaybackState(): Promise<PlaybackStateResponse>`
|
|
206
|
+
|
|
207
|
+
Get playback flags (is_playing, shuffle, repeat).
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const state = await client.getPlaybackState();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
##### `getPlaybackQueue(): Promise<PlaybackQueueResponse>`
|
|
214
|
+
|
|
215
|
+
Get the current playback queue and metadata.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const queue = await client.getPlaybackQueue();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
##### `getUserPublic(): Promise<UserPublicResponse>`
|
|
222
|
+
|
|
223
|
+
Get the signed-in user's public profile snapshot.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const user = await client.getUserPublic();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Write Endpoints
|
|
230
|
+
|
|
231
|
+
All write endpoints require `write` permission.
|
|
232
|
+
|
|
233
|
+
##### `play(trackId?: string): Promise<PlayResponse>`
|
|
234
|
+
|
|
235
|
+
Start playback or resume. Optionally specify a track ID.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
await client.play();
|
|
239
|
+
await client.play('OrZoMJcXje2');
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
##### `pause(): Promise<PauseResponse>`
|
|
243
|
+
|
|
244
|
+
Pause playback.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
await client.pause();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
##### `next(): Promise<NextResponse>`
|
|
251
|
+
|
|
252
|
+
Skip to next track.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
await client.next();
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
##### `seek(position: number): Promise<SeekResponse>`
|
|
259
|
+
|
|
260
|
+
Seek to a position in milliseconds.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
await client.seek(60000);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
##### `toggle(): Promise<ToggleResponse>`
|
|
267
|
+
|
|
268
|
+
Toggle between play and pause.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
await client.toggle();
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### WebSocket Methods
|
|
275
|
+
|
|
276
|
+
##### `connectWebSocket(): Promise<void>`
|
|
277
|
+
|
|
278
|
+
Establish WebSocket connection to receive real-time events.
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
await client.connectWebSocket();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
##### `disconnectWebSocket(): Promise<void>`
|
|
285
|
+
|
|
286
|
+
Close WebSocket connection.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
await client.disconnectWebSocket();
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
##### `isWSConnected(): boolean`
|
|
293
|
+
|
|
294
|
+
Check if WebSocket is connected.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
if (client.isWSConnected()) {
|
|
298
|
+
console.log('Receiving real-time updates');
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### WebSocket Events
|
|
303
|
+
|
|
304
|
+
##### `track_changed`
|
|
305
|
+
|
|
306
|
+
Emitted when the track changes.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
client.on('track_changed', (data: TrackChangedData) => {
|
|
310
|
+
console.log(data.id, data.title, data.artist);
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
##### `playback_state_changed`
|
|
315
|
+
|
|
316
|
+
Emitted when playback state changes.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
client.on('playback_state_changed', (data: PlaybackStateChangedData) => {
|
|
320
|
+
console.log('is_playing:', data.is_playing);
|
|
321
|
+
console.log('shuffle:', data.shuffle);
|
|
322
|
+
console.log('repeat:', data.repeat);
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
##### `ws:connected`
|
|
327
|
+
|
|
328
|
+
Emitted when WebSocket connects.
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
client.on('ws:connected', () => {
|
|
332
|
+
console.log('WebSocket connected');
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
##### `ws:disconnected`
|
|
337
|
+
|
|
338
|
+
Emitted when WebSocket disconnects.
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
client.on('ws:disconnected', () => {
|
|
342
|
+
console.log('WebSocket disconnected');
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
##### `ws:connecting`
|
|
347
|
+
|
|
348
|
+
Emitted before each connection attempt.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
client.on('ws:connecting', () => {
|
|
352
|
+
console.log('Attempting to connect...');
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
##### `ws:error`
|
|
357
|
+
|
|
358
|
+
Emitted on WebSocket errors.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
client.on('ws:error', (error: Error) => {
|
|
362
|
+
console.error('WebSocket error:', error.message);
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Error Handling
|
|
367
|
+
|
|
368
|
+
The SDK provides specific error classes to help identify issues:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import {
|
|
372
|
+
VeloraError,
|
|
373
|
+
AuthError,
|
|
374
|
+
NetworkError,
|
|
375
|
+
ValidationError,
|
|
376
|
+
TimeoutError,
|
|
377
|
+
} from 'velora-node-sdk';
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await client.getCurrentTrack();
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (error instanceof AuthError) {
|
|
383
|
+
console.error('Permission denied or invalid token');
|
|
384
|
+
} else if (error instanceof NetworkError) {
|
|
385
|
+
console.error('Network issue:', error.message);
|
|
386
|
+
} else if (error instanceof ValidationError) {
|
|
387
|
+
console.error('Invalid input:', error.message);
|
|
388
|
+
} else if (error instanceof TimeoutError) {
|
|
389
|
+
console.error('Request timed out');
|
|
390
|
+
} else if (error instanceof VeloraError) {
|
|
391
|
+
console.error('Velora error:', error.message, error.code);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Error Classes
|
|
397
|
+
|
|
398
|
+
- **VeloraError** — Base error class; all Velora errors extend this
|
|
399
|
+
- **AuthError** — Authentication or authorization failure (401/403)
|
|
400
|
+
- **NetworkError** — Network or connection issue
|
|
401
|
+
- **ValidationError** — Invalid input or request body
|
|
402
|
+
- **TimeoutError** — Request timeout
|
|
403
|
+
- **RateLimitError** — Rate limit exceeded (429)
|
|
404
|
+
- **RequestError** — HTTP error (4xx or 5xx)
|
|
405
|
+
|
|
406
|
+
## Configuration
|
|
407
|
+
|
|
408
|
+
### HTTP Options
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
const client = new VeloraClient({
|
|
412
|
+
httpOptions: {
|
|
413
|
+
timeout: 30000,
|
|
414
|
+
maxRetries: 3,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- `timeout` — Request timeout in milliseconds (default: 30000)
|
|
420
|
+
- `maxRetries` — Max retry attempts for 5xx errors (default: 3)
|
|
421
|
+
|
|
422
|
+
### WebSocket Options
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const client = new VeloraClient({
|
|
426
|
+
wsOptions: {
|
|
427
|
+
initialBackoffMs: 1000,
|
|
428
|
+
maxBackoffMs: 60000,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
- `initialBackoffMs` — Initial reconnect backoff (default: 1000ms)
|
|
434
|
+
- `maxBackoffMs` — Max reconnect backoff (default: 60000ms)
|
|
435
|
+
|
|
436
|
+
## Connection Lifecycle
|
|
437
|
+
|
|
438
|
+
### Basic Flow
|
|
439
|
+
|
|
440
|
+
1. Create client
|
|
441
|
+
2. Register app (or use pre-existing token)
|
|
442
|
+
3. Poll for user approval (if registering)
|
|
443
|
+
4. Call `connect()` to verify health and optionally start WebSocket
|
|
444
|
+
5. Use HTTP or WebSocket APIs
|
|
445
|
+
6. Call `disconnect()` when done
|
|
446
|
+
|
|
447
|
+
### With Pre-existing Token
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const client = new VeloraClient({
|
|
451
|
+
token: 'velora_...',
|
|
452
|
+
requestedPermissions: ['read', 'write'],
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const track = await client.getCurrentTrack();
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Error Recovery
|
|
459
|
+
|
|
460
|
+
HTTP client automatically retries on 5xx errors with exponential backoff. WebSocket client auto-reconnects on disconnect with backoff.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
client.on('ws:error', (error) => {
|
|
464
|
+
console.log('Disconnected, will attempt to reconnect...');
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## TypeScript Support
|
|
469
|
+
|
|
470
|
+
The SDK is written in TypeScript and includes full type definitions. All APIs are strictly typed:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import { VeloraClient, CurrentTrackResponse, TrackChangedData } from 'velora-node-sdk';
|
|
474
|
+
|
|
475
|
+
const client = new VeloraClient();
|
|
476
|
+
|
|
477
|
+
const response: CurrentTrackResponse = await client.getCurrentTrack();
|
|
478
|
+
|
|
479
|
+
client.on('track_changed', (data: TrackChangedData) => {
|
|
480
|
+
console.log(data.title);
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Permissions
|
|
485
|
+
|
|
486
|
+
- `read` — Access to all GET endpoints and WebSocket
|
|
487
|
+
- `write` — Access to all POST endpoints (play, pause, seek, etc.)
|
|
488
|
+
|
|
489
|
+
Check permissions at runtime:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
if (client.getPermissions().includes('write')) {
|
|
493
|
+
await client.play();
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## Localhost only
|
|
498
|
+
|
|
499
|
+
The Velora local API only accepts connections from `127.0.0.1` or `::1`. The SDK enforces this at the client level.
|
|
500
|
+
|
|
501
|
+
## License
|
|
502
|
+
|
|
503
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PermissionType, RegisterAppRequest } from '../types';
|
|
2
|
+
export declare class CredentialManager {
|
|
3
|
+
private token;
|
|
4
|
+
private permissions;
|
|
5
|
+
private appName;
|
|
6
|
+
private baseUrl;
|
|
7
|
+
constructor(baseUrl: string, initialToken?: string);
|
|
8
|
+
getToken(): string | null;
|
|
9
|
+
setToken(token: string, permissions?: PermissionType[]): void;
|
|
10
|
+
getPermissions(): PermissionType[];
|
|
11
|
+
hasPermission(permission: PermissionType): boolean;
|
|
12
|
+
getAuthHeader(): string | null;
|
|
13
|
+
isAuthenticated(): boolean;
|
|
14
|
+
clear(): void;
|
|
15
|
+
register(appConfig: RegisterAppRequest): Promise<string>;
|
|
16
|
+
requestAccessToken(requestId: string, maxWaitMs?: number): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CredentialManager = void 0;
|
|
4
|
+
const errors_1 = require("../types/errors");
|
|
5
|
+
class CredentialManager {
|
|
6
|
+
constructor(baseUrl, initialToken) {
|
|
7
|
+
this.permissions = [];
|
|
8
|
+
this.appName = null;
|
|
9
|
+
this.baseUrl = baseUrl;
|
|
10
|
+
this.token = initialToken || null;
|
|
11
|
+
}
|
|
12
|
+
getToken() {
|
|
13
|
+
return this.token;
|
|
14
|
+
}
|
|
15
|
+
setToken(token, permissions) {
|
|
16
|
+
this.token = token;
|
|
17
|
+
if (permissions) {
|
|
18
|
+
this.permissions = permissions;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
getPermissions() {
|
|
22
|
+
return this.permissions;
|
|
23
|
+
}
|
|
24
|
+
hasPermission(permission) {
|
|
25
|
+
return this.permissions.includes(permission);
|
|
26
|
+
}
|
|
27
|
+
getAuthHeader() {
|
|
28
|
+
if (!this.token)
|
|
29
|
+
return null;
|
|
30
|
+
return `Bearer ${this.token}`;
|
|
31
|
+
}
|
|
32
|
+
isAuthenticated() {
|
|
33
|
+
return this.token !== null && this.token.length > 0;
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
this.token = null;
|
|
37
|
+
this.permissions = [];
|
|
38
|
+
this.appName = null;
|
|
39
|
+
}
|
|
40
|
+
async register(appConfig) {
|
|
41
|
+
if (!appConfig.app.name) {
|
|
42
|
+
throw new errors_1.ValidationError('app.name is required', 'app.name');
|
|
43
|
+
}
|
|
44
|
+
const payload = {
|
|
45
|
+
app: {
|
|
46
|
+
name: appConfig.app.name,
|
|
47
|
+
...(appConfig.app.description && { description: appConfig.app.description }),
|
|
48
|
+
...(appConfig.app.developer && { developer: appConfig.app.developer }),
|
|
49
|
+
...(appConfig.app.website && { website: appConfig.app.website }),
|
|
50
|
+
...(appConfig.app.icon && { icon: appConfig.app.icon }),
|
|
51
|
+
},
|
|
52
|
+
permissions: appConfig.permissions || ['read'],
|
|
53
|
+
};
|
|
54
|
+
this.appName = appConfig.app.name;
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(`${this.baseUrl}/register`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(payload),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const body = await response.text();
|
|
65
|
+
let errorMsg = body;
|
|
66
|
+
try {
|
|
67
|
+
const errorJson = JSON.parse(body);
|
|
68
|
+
errorMsg = errorJson.error || body;
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
throw new errors_1.RequestError(`Registration failed: ${errorMsg}`, response.status);
|
|
72
|
+
}
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
return data.request_id;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof errors_1.RequestError)
|
|
78
|
+
throw error;
|
|
79
|
+
throw new errors_1.RequestError(error instanceof Error ? error.message : 'Registration failed', 0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async requestAccessToken(requestId, maxWaitMs = 300000) {
|
|
83
|
+
if (!requestId) {
|
|
84
|
+
throw new errors_1.ValidationError('request_id is required', 'request_id');
|
|
85
|
+
}
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
const pollIntervalMs = 1000;
|
|
88
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(`${this.baseUrl}/request-status?request_id=${encodeURIComponent(requestId)}`);
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new errors_1.RequestError(`Request status failed`, response.status);
|
|
93
|
+
}
|
|
94
|
+
const data = await response.json();
|
|
95
|
+
if (data.status === 'approved') {
|
|
96
|
+
const approvedData = data;
|
|
97
|
+
this.token = approvedData.access_token;
|
|
98
|
+
this.permissions = approvedData.permissions;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (data.status === 'denied') {
|
|
102
|
+
throw new errors_1.AuthError('Registration request was denied');
|
|
103
|
+
}
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof (errors_1.ValidationError || errors_1.AuthError || errors_1.RequestError)) {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
throw new errors_1.RequestError(error instanceof Error ? error.message : 'Failed to get access token', 0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw new errors_1.RequestError('Request access token timeout', 0);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.CredentialManager = CredentialManager;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CredentialManager } from './CredentialManager';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CredentialManager = void 0;
|
|
4
|
+
var CredentialManager_1 = require("./CredentialManager");
|
|
5
|
+
Object.defineProperty(exports, "CredentialManager", { enumerable: true, get: function () { return CredentialManager_1.CredentialManager; } });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ClientConfig, RegisterAppRequest, HealthResponse, CurrentTrackResponse, PlaybackStateResponse, PlaybackQueueResponse, UserPublicResponse, PlayResponse, PauseResponse, NextResponse, SeekResponse, ToggleResponse } from '../types';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
export declare class VeloraClient extends EventEmitter {
|
|
4
|
+
private credentialManager;
|
|
5
|
+
private httpClient;
|
|
6
|
+
private wsClient;
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private wsBaseUrl;
|
|
9
|
+
constructor(config?: ClientConfig);
|
|
10
|
+
isAuthenticated(): boolean;
|
|
11
|
+
registerApp(appConfig: RegisterAppRequest): Promise<string>;
|
|
12
|
+
requestAccessToken(requestId: string, maxWaitMs?: number): Promise<void>;
|
|
13
|
+
getHealth(): Promise<HealthResponse>;
|
|
14
|
+
getCurrentTrack(): Promise<CurrentTrackResponse>;
|
|
15
|
+
getPlaybackState(): Promise<PlaybackStateResponse>;
|
|
16
|
+
getPlaybackQueue(): Promise<PlaybackQueueResponse>;
|
|
17
|
+
getUserPublic(): Promise<UserPublicResponse>;
|
|
18
|
+
play(trackId?: string): Promise<PlayResponse>;
|
|
19
|
+
pause(): Promise<PauseResponse>;
|
|
20
|
+
next(): Promise<NextResponse>;
|
|
21
|
+
seek(position: number): Promise<SeekResponse>;
|
|
22
|
+
toggle(): Promise<ToggleResponse>;
|
|
23
|
+
connectWebSocket(): Promise<void>;
|
|
24
|
+
disconnectWebSocket(): Promise<void>;
|
|
25
|
+
isWSConnected(): boolean;
|
|
26
|
+
connect(): Promise<void>;
|
|
27
|
+
disconnect(): Promise<void>;
|
|
28
|
+
clearCredentials(): void;
|
|
29
|
+
getToken(): string | null;
|
|
30
|
+
setToken(token: string, permissions?: ('read' | 'write')[]): void;
|
|
31
|
+
getPermissions(): ('read' | 'write')[];
|
|
32
|
+
}
|