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 ADDED
@@ -0,0 +1,6 @@
1
+ Copyright (c) 2025-2026 CrickDevs
2
+
3
+ All rights reserved.
4
+
5
+ This software is provided for use only. You may not copy, modify,
6
+ distribute, sublicense, or sell this software without explicit permission.
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
+ }