@bzenky/spoti 0.1.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 +21 -0
- package/README.md +137 -0
- package/dist/app.d.ts +14 -0
- package/dist/app.js +122 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/callback.d.ts +7 -0
- package/dist/auth/callback.js +79 -0
- package/dist/auth/callback.js.map +1 -0
- package/dist/auth/config.d.ts +7 -0
- package/dist/auth/config.js +31 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/pkce.d.ts +7 -0
- package/dist/auth/pkce.js +15 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/spotify-auth-client.d.ts +38 -0
- package/dist/auth/spotify-auth-client.js +152 -0
- package/dist/auth/spotify-auth-client.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -0
- package/dist/services/auth.service.d.ts +36 -0
- package/dist/services/auth.service.js +127 -0
- package/dist/services/auth.service.js.map +1 -0
- package/dist/services/models.d.ts +20 -0
- package/dist/services/models.js +2 -0
- package/dist/services/models.js.map +1 -0
- package/dist/services/player.service.d.ts +12 -0
- package/dist/services/player.service.js +45 -0
- package/dist/services/player.service.js.map +1 -0
- package/dist/services/search.service.d.ts +7 -0
- package/dist/services/search.service.js +28 -0
- package/dist/services/search.service.js.map +1 -0
- package/dist/spotify/client.d.ts +27 -0
- package/dist/spotify/client.js +113 -0
- package/dist/spotify/client.js.map +1 -0
- package/dist/spotify/scopes.d.ts +1 -0
- package/dist/spotify/scopes.js +5 -0
- package/dist/spotify/scopes.js.map +1 -0
- package/dist/spotify/types.d.ts +50 -0
- package/dist/spotify/types.js +2 -0
- package/dist/spotify/types.js.map +1 -0
- package/dist/storage/credentials.d.ts +28 -0
- package/dist/storage/credentials.js +94 -0
- package/dist/storage/credentials.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/ui/output.d.ts +8 -0
- package/dist/ui/output.js +21 -0
- package/dist/ui/output.js.map +1 -0
- package/dist/ui/prompts.d.ts +2 -0
- package/dist/ui/prompts.js +22 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/utils/errors.d.ts +26 -0
- package/dist/utils/errors.js +45 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/time.d.ts +2 -0
- package/dist/utils/time.js +13 -0
- package/dist/utils/time.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 spoti contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# spoti
|
|
2
|
+
|
|
3
|
+
A local-first command-line client for controlling Spotify through Spotify Connect.
|
|
4
|
+
|
|
5
|
+
> `spoti` is an independent project and is not affiliated with, endorsed by, or sponsored by Spotify AB.
|
|
6
|
+
|
|
7
|
+
`spoti` controls playback on an existing Spotify client or Connect device; it does not stream audio itself.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js 20 or newer
|
|
12
|
+
- A Spotify developer application and client ID
|
|
13
|
+
- Spotify Premium for playback-control commands
|
|
14
|
+
- A Spotify client or Connect device available for playback
|
|
15
|
+
|
|
16
|
+
## Spotify application setup
|
|
17
|
+
|
|
18
|
+
Create an application in the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard) and register this exact redirect URI:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
http://127.0.0.1:43821/callback
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Export the application's client ID before running authentication:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export SPOTIFY_CLIENT_ID="your-client-id"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To use a different local callback, register it in the same Spotify application and set:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export SPOTIFY_REDIRECT_URI="http://127.0.0.1:5000/callback"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The redirect URI must use HTTPS except for local development, where an explicit `http://127.0.0.1` URI is allowed. Do not use `localhost` or wildcard redirect URIs. No client secret is needed or accepted by `spoti`; authentication uses Authorization Code with PKCE.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Once published to npm:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install --global @bzenky/spoti
|
|
44
|
+
spoti --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Install for development
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install
|
|
51
|
+
npm run build
|
|
52
|
+
npm link
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then verify the executable:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
spoti --help
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
You can also run commands without linking:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm run dev -- --help
|
|
65
|
+
npm run dev -- status
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Authenticate once through Spotify's browser authorization page:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
spoti login
|
|
74
|
+
spoti status
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Control playback:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
spoti now
|
|
81
|
+
spoti play "Numb"
|
|
82
|
+
spoti play "Numb" --first
|
|
83
|
+
spoti pause
|
|
84
|
+
spoti resume
|
|
85
|
+
spoti next
|
|
86
|
+
spoti previous
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Search without starting playback:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
spoti search "Breaking the Habit"
|
|
93
|
+
spoti search "Breaking the Habit" --limit 5
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Remove local credentials:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
spoti logout
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When attached to an interactive terminal, `spoti play <query>` asks you to select a result. In non-interactive usage it chooses the first result automatically; `--first` makes that behavior explicit.
|
|
103
|
+
|
|
104
|
+
## Credentials
|
|
105
|
+
|
|
106
|
+
Credentials are stored locally in:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
$XDG_CONFIG_HOME/spoti/credentials.json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
or, when `XDG_CONFIG_HOME` is not set:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
~/.config/spoti/credentials.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The credentials file is created with user-only permissions (`0600`). Access tokens refresh automatically. Keep `SPOTIFY_CLIENT_ID` available in the environment because Spotify requires it during token refresh. Never provide or store a Spotify client secret in `spoti`.
|
|
119
|
+
|
|
120
|
+
## Spotify API policy
|
|
121
|
+
|
|
122
|
+
`spoti` uses Spotify data only for immediate command output and playback control. It does not persist Spotify catalog content, use Spotify data for machine-learning training, or download audio. Spotify content and links remain attributed to Spotify.
|
|
123
|
+
|
|
124
|
+
Endpoint work must be checked against Spotify's [official OpenAPI specification](https://developer.spotify.com/reference/web-api/open-api-schema.yaml) and [Developer Terms](https://developer.spotify.com/terms).
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm run typecheck
|
|
130
|
+
npm run lint
|
|
131
|
+
npm test
|
|
132
|
+
npm run build
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Current scope
|
|
136
|
+
|
|
137
|
+
Version `0.1.0` supports authentication, current playback, track search, direct track playback, pause/resume, and next/previous controls. Queue, volume, seek, shuffle, repeat, device switching, library features, JSON output, and a TUI are planned for later releases.
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { AuthService } from './services/auth.service.js';
|
|
3
|
+
import type { PlayerService } from './services/player.service.js';
|
|
4
|
+
import type { SearchService } from './services/search.service.js';
|
|
5
|
+
import { type Output } from './ui/output.js';
|
|
6
|
+
import { selectTrack } from './ui/prompts.js';
|
|
7
|
+
export interface AppDependencies {
|
|
8
|
+
auth: AuthService;
|
|
9
|
+
player: PlayerService;
|
|
10
|
+
search: SearchService;
|
|
11
|
+
output: Output;
|
|
12
|
+
chooseTrack?: typeof selectTrack;
|
|
13
|
+
}
|
|
14
|
+
export declare function createProgram(dependencies: AppDependencies): Command;
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { formatPlayback, formatTrack } from './ui/output.js';
|
|
3
|
+
import { selectTrack } from './ui/prompts.js';
|
|
4
|
+
export function createProgram(dependencies) {
|
|
5
|
+
const program = new Command();
|
|
6
|
+
const chooseTrack = dependencies.chooseTrack ?? selectTrack;
|
|
7
|
+
program
|
|
8
|
+
.name('spoti')
|
|
9
|
+
.description('Control Spotify from your terminal')
|
|
10
|
+
.version('0.1.0');
|
|
11
|
+
program
|
|
12
|
+
.command('login')
|
|
13
|
+
.description('Log in to Spotify')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
dependencies.output.log('Opening Spotify authorization in your browser…');
|
|
16
|
+
const user = await dependencies.auth.login();
|
|
17
|
+
dependencies.output.log(`✓ Logged in as ${user.displayName}`);
|
|
18
|
+
});
|
|
19
|
+
program
|
|
20
|
+
.command('logout')
|
|
21
|
+
.description('Remove locally stored Spotify credentials')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
await dependencies.auth.logout();
|
|
24
|
+
dependencies.output.log('✓ Logged out');
|
|
25
|
+
});
|
|
26
|
+
program
|
|
27
|
+
.command('status')
|
|
28
|
+
.description('Show authentication status')
|
|
29
|
+
.action(async () => {
|
|
30
|
+
if (!(await dependencies.auth.isAuthenticated())) {
|
|
31
|
+
dependencies.output.log('Not logged in.\n\nRun: spoti login');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const user = await dependencies.auth.getCurrentUser();
|
|
35
|
+
dependencies.output.log(`✓ Logged in as ${user.displayName}`);
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('now')
|
|
39
|
+
.description('Show the current Spotify playback')
|
|
40
|
+
.action(async () => {
|
|
41
|
+
const playback = await dependencies.player.getCurrentPlayback();
|
|
42
|
+
dependencies.output.log(playback ? formatPlayback(playback) : 'Nothing is currently playing.');
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command('pause')
|
|
46
|
+
.description('Pause playback')
|
|
47
|
+
.action(async () => {
|
|
48
|
+
await dependencies.player.pause();
|
|
49
|
+
dependencies.output.log('⏸ Paused');
|
|
50
|
+
});
|
|
51
|
+
program
|
|
52
|
+
.command('resume')
|
|
53
|
+
.description('Resume playback')
|
|
54
|
+
.action(async () => {
|
|
55
|
+
await dependencies.player.resume();
|
|
56
|
+
dependencies.output.log('▶ Resumed');
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command('next')
|
|
60
|
+
.alias('n')
|
|
61
|
+
.description('Skip to the next track')
|
|
62
|
+
.action(async () => {
|
|
63
|
+
await dependencies.player.next();
|
|
64
|
+
dependencies.output.log('⏭ Skipped to next track');
|
|
65
|
+
});
|
|
66
|
+
program
|
|
67
|
+
.command('previous')
|
|
68
|
+
.alias('prev')
|
|
69
|
+
.description('Return to the previous track')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
await dependencies.player.previous();
|
|
72
|
+
dependencies.output.log('⏮ Returned to previous track');
|
|
73
|
+
});
|
|
74
|
+
program
|
|
75
|
+
.command('search')
|
|
76
|
+
.description('Search Spotify tracks')
|
|
77
|
+
.argument('<query...>', 'track name to search for')
|
|
78
|
+
.option('-l, --limit <number>', 'maximum number of results', parseLimit, 10)
|
|
79
|
+
.action(async (queryParts, options) => {
|
|
80
|
+
const query = queryParts.join(' ');
|
|
81
|
+
const tracks = await dependencies.search.searchTracks(query, options.limit);
|
|
82
|
+
if (tracks.length === 0) {
|
|
83
|
+
dependencies.output.log(`No tracks found for "${query}".`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
dependencies.output.log(tracks.map((track, index) => formatTrack(track, index)).join('\n'));
|
|
87
|
+
});
|
|
88
|
+
program
|
|
89
|
+
.command('play')
|
|
90
|
+
.description('Search for and play a track, or resume with no query')
|
|
91
|
+
.argument('[query...]', 'track name to search for')
|
|
92
|
+
.option('--first', 'play the first search result without prompting')
|
|
93
|
+
.action(async (queryParts, options) => {
|
|
94
|
+
const query = queryParts.join(' ').trim();
|
|
95
|
+
if (!query) {
|
|
96
|
+
await dependencies.player.resume();
|
|
97
|
+
dependencies.output.log('▶ Resumed');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const tracks = await dependencies.search.searchTracks(query);
|
|
101
|
+
if (tracks.length === 0) {
|
|
102
|
+
dependencies.output.log(`No tracks found for "${query}".`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const track = options.first ? tracks[0] : await chooseTrack(tracks);
|
|
106
|
+
if (!track) {
|
|
107
|
+
dependencies.output.log('Selection cancelled.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await dependencies.player.playTrack(track.uri);
|
|
111
|
+
dependencies.output.log(`▶ Playing ${track.name} — ${track.artists.join(', ')}`);
|
|
112
|
+
});
|
|
113
|
+
return program;
|
|
114
|
+
}
|
|
115
|
+
function parseLimit(value) {
|
|
116
|
+
const limit = /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
117
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 10) {
|
|
118
|
+
throw new Error('Search limit must be an integer between 1 and 10.');
|
|
119
|
+
}
|
|
120
|
+
return limit;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type BrowserOpener = (url: string) => Promise<unknown>;
|
|
2
|
+
export interface AuthorizationCallbackOptions {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
openBrowser?: BrowserOpener;
|
|
5
|
+
}
|
|
6
|
+
export declare function requestAuthorizationCode(authorizationUrl: string, redirectUri: string, expectedState: string, options?: AuthorizationCallbackOptions): Promise<string>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import { AppError, AuthorizationDeniedError, toError, } from '../utils/errors.js';
|
|
4
|
+
import { validateRedirectUri } from './config.js';
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
6
|
+
export function requestAuthorizationCode(authorizationUrl, redirectUri, expectedState, options = {}) {
|
|
7
|
+
const callbackUrl = validateRedirectUri(redirectUri);
|
|
8
|
+
const port = Number(callbackUrl.port);
|
|
9
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
10
|
+
const openBrowser = options.openBrowser ?? open;
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let settled = false;
|
|
13
|
+
let timer;
|
|
14
|
+
const server = createServer((request, response) => {
|
|
15
|
+
const requestUrl = new URL(request.url ?? '/', callbackUrl.origin);
|
|
16
|
+
if (request.method !== 'GET' || requestUrl.pathname !== callbackUrl.pathname) {
|
|
17
|
+
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
18
|
+
response.end('Not found');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const finish = (error, code) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
if (timer)
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
server.close();
|
|
28
|
+
if (error)
|
|
29
|
+
reject(error);
|
|
30
|
+
else if (code)
|
|
31
|
+
resolve(code);
|
|
32
|
+
else
|
|
33
|
+
reject(new AppError('Spotify authorization returned no code.'));
|
|
34
|
+
};
|
|
35
|
+
if (requestUrl.searchParams.get('state') !== expectedState) {
|
|
36
|
+
response.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' });
|
|
37
|
+
response.end('Authorization state mismatch. You can close this window.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const authorizationError = requestUrl.searchParams.get('error');
|
|
41
|
+
if (authorizationError) {
|
|
42
|
+
const description = requestUrl.searchParams.get('error_description');
|
|
43
|
+
response.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' });
|
|
44
|
+
response.end('Spotify authorization was denied. You can close this window.');
|
|
45
|
+
finish(new AuthorizationDeniedError(description || `Spotify authorization failed: ${authorizationError}`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const code = requestUrl.searchParams.get('code');
|
|
49
|
+
if (!code) {
|
|
50
|
+
response.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' });
|
|
51
|
+
response.end('No authorization code was received. You can close this window.');
|
|
52
|
+
finish(new AppError('Spotify authorization returned no code.'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
|
|
56
|
+
response.end('Authorization complete. You can close this window.');
|
|
57
|
+
finish(null, code);
|
|
58
|
+
});
|
|
59
|
+
const fail = (error) => {
|
|
60
|
+
if (settled)
|
|
61
|
+
return;
|
|
62
|
+
settled = true;
|
|
63
|
+
if (timer)
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
if (server.listening)
|
|
66
|
+
server.close();
|
|
67
|
+
reject(toError(error));
|
|
68
|
+
};
|
|
69
|
+
server.once('error', fail);
|
|
70
|
+
server.listen(port, '127.0.0.1', () => {
|
|
71
|
+
timer = setTimeout(() => {
|
|
72
|
+
fail(new AuthorizationDeniedError('Spotify authorization timed out after 2 minutes.'));
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
timer.unref();
|
|
75
|
+
void openBrowser(authorizationUrl).catch(fail);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=callback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/auth/callback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const DEFAULT_REDIRECT_URI = "http://127.0.0.1:43821/callback";
|
|
2
|
+
export interface AuthConfig {
|
|
3
|
+
clientId: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadAuthConfig(environment?: NodeJS.ProcessEnv): AuthConfig;
|
|
7
|
+
export declare function validateRedirectUri(redirectUri: string): URL;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ConfigurationError } from '../utils/errors.js';
|
|
2
|
+
export const DEFAULT_REDIRECT_URI = 'http://127.0.0.1:43821/callback';
|
|
3
|
+
export function loadAuthConfig(environment = process.env) {
|
|
4
|
+
const clientId = environment.SPOTIFY_CLIENT_ID?.trim();
|
|
5
|
+
if (!clientId) {
|
|
6
|
+
throw new ConfigurationError('SPOTIFY_CLIENT_ID is required. Set it to your Spotify application client ID.');
|
|
7
|
+
}
|
|
8
|
+
const redirectUri = environment.SPOTIFY_REDIRECT_URI?.trim() || DEFAULT_REDIRECT_URI;
|
|
9
|
+
validateRedirectUri(redirectUri);
|
|
10
|
+
return { clientId, redirectUri };
|
|
11
|
+
}
|
|
12
|
+
export function validateRedirectUri(redirectUri) {
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(redirectUri);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new ConfigurationError('SPOTIFY_REDIRECT_URI must be a valid URL.');
|
|
19
|
+
}
|
|
20
|
+
if (url.protocol !== 'http:' ||
|
|
21
|
+
url.hostname !== '127.0.0.1' ||
|
|
22
|
+
!url.port ||
|
|
23
|
+
url.username ||
|
|
24
|
+
url.password ||
|
|
25
|
+
url.search ||
|
|
26
|
+
url.hash) {
|
|
27
|
+
throw new ConfigurationError('SPOTIFY_REDIRECT_URI must be an HTTP URL on 127.0.0.1 with an explicit port, path, and no query or fragment.');
|
|
28
|
+
}
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,CAAC,MAAM,oBAAoB,GAAG,iCAAiC,CAAC;AAOtE,MAAM,UAAU,cAAc,CAC5B,cAAiC,OAAO,CAAC,GAAG;IAE5C,MAAM,QAAQ,GAAG,WAAW,CAAC,iBAAiB,EAAE,IAAI,EAAE,CAAC;IACvD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,kBAAkB,CAC1B,8EAA8E,CAC/E,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,WAAW,CAAC,oBAAoB,EAAE,IAAI,EAAE,IAAI,oBAAoB,CAAC;IACrF,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAEjC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,kBAAkB,CAAC,2CAA2C,CAAC,CAAC;IAC5E,CAAC;IAED,IACE,GAAG,CAAC,QAAQ,KAAK,OAAO;QACxB,GAAG,CAAC,QAAQ,KAAK,WAAW;QAC5B,CAAC,GAAG,CAAC,IAAI;QACT,GAAG,CAAC,QAAQ;QACZ,GAAG,CAAC,QAAQ;QACZ,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,IAAI,EACR,CAAC;QACD,MAAM,IAAI,kBAAkB,CAC1B,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { requestAuthorizationCode } from './callback.js';
|
|
2
|
+
export type { AuthorizationCallbackOptions } from './callback.js';
|
|
3
|
+
export { DEFAULT_REDIRECT_URI, loadAuthConfig, validateRedirectUri, } from './config.js';
|
|
4
|
+
export type { AuthConfig } from './config.js';
|
|
5
|
+
export { createCodeChallenge, generateOAuthState, generatePkcePair, } from './pkce.js';
|
|
6
|
+
export type { PkcePair } from './pkce.js';
|
|
7
|
+
export { SpotifyAuthClient } from './spotify-auth-client.js';
|
|
8
|
+
export type { ExchangeCodeInput, RefreshTokenInput, SpotifyAuthApi, TokenSet, } from './spotify-auth-client.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { requestAuthorizationCode } from './callback.js';
|
|
2
|
+
export { DEFAULT_REDIRECT_URI, loadAuthConfig, validateRedirectUri, } from './config.js';
|
|
3
|
+
export { createCodeChallenge, generateOAuthState, generatePkcePair, } from './pkce.js';
|
|
4
|
+
export { SpotifyAuthClient } from './spotify-auth-client.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
export function createCodeChallenge(verifier) {
|
|
3
|
+
return createHash('sha256').update(verifier, 'ascii').digest('base64url');
|
|
4
|
+
}
|
|
5
|
+
export function generatePkcePair() {
|
|
6
|
+
const verifier = randomBytes(64).toString('base64url');
|
|
7
|
+
return {
|
|
8
|
+
verifier,
|
|
9
|
+
challenge: createCodeChallenge(verifier),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function generateOAuthState() {
|
|
13
|
+
return randomBytes(32).toString('base64url');
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAOtD,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACvD,OAAO;QACL,QAAQ;QACR,SAAS,EAAE,mBAAmB,CAAC,QAAQ,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { UserProfile } from '../services/models.js';
|
|
2
|
+
import { AppError } from '../utils/errors.js';
|
|
3
|
+
type Sleep = (milliseconds: number) => Promise<void>;
|
|
4
|
+
export interface TokenSet {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
refreshToken?: string;
|
|
7
|
+
expiresIn: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ExchangeCodeInput {
|
|
10
|
+
clientId: string;
|
|
11
|
+
code: string;
|
|
12
|
+
redirectUri: string;
|
|
13
|
+
codeVerifier: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RefreshTokenInput {
|
|
16
|
+
clientId: string;
|
|
17
|
+
refreshToken: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class OAuthTokenError extends AppError {
|
|
20
|
+
readonly code: string;
|
|
21
|
+
constructor(code: string, message: string);
|
|
22
|
+
}
|
|
23
|
+
export interface SpotifyAuthApi {
|
|
24
|
+
exchangeCode(input: ExchangeCodeInput): Promise<TokenSet>;
|
|
25
|
+
refreshAccessToken(input: RefreshTokenInput): Promise<TokenSet>;
|
|
26
|
+
getCurrentUser(accessToken: string): Promise<UserProfile>;
|
|
27
|
+
}
|
|
28
|
+
export declare class SpotifyAuthClient implements SpotifyAuthApi {
|
|
29
|
+
private readonly fetchImplementation;
|
|
30
|
+
private readonly sleeper;
|
|
31
|
+
constructor(fetchImplementation?: typeof globalThis.fetch, sleeper?: Sleep);
|
|
32
|
+
exchangeCode(input: ExchangeCodeInput): Promise<TokenSet>;
|
|
33
|
+
refreshAccessToken(input: RefreshTokenInput): Promise<TokenSet>;
|
|
34
|
+
getCurrentUser(accessToken: string): Promise<UserProfile>;
|
|
35
|
+
private requestToken;
|
|
36
|
+
private fetchWithTimeout;
|
|
37
|
+
}
|
|
38
|
+
export {};
|