@louisraetz/steamidled 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 +21 -0
- package/README.md +154 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +252 -0
- package/dist/steam/auth.d.ts +8 -0
- package/dist/steam/auth.js +142 -0
- package/dist/steam/client.d.ts +22 -0
- package/dist/steam/client.js +118 -0
- package/dist/storage/credentials.d.ts +5 -0
- package/dist/storage/credentials.js +39 -0
- package/dist/storage/favorites.d.ts +6 -0
- package/dist/storage/favorites.js +72 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.js +1 -0
- package/dist/ui/display.d.ts +8 -0
- package/dist/ui/display.js +99 -0
- package/dist/ui/gameSelector.d.ts +7 -0
- package/dist/ui/gameSelector.js +192 -0
- package/dist/ui/prompts.d.ts +5 -0
- package/dist/ui/prompts.js +79 -0
- package/package.json +49 -0
- package/src/index.ts +378 -0
- package/src/steam/auth.ts +180 -0
- package/src/steam/client.ts +149 -0
- package/src/storage/credentials.ts +46 -0
- package/src/storage/favorites.ts +86 -0
- package/src/types/index.ts +38 -0
- package/src/types/modules.d.ts +56 -0
- package/src/ui/display.ts +127 -0
- package/src/ui/gameSelector.ts +244 -0
- package/src/ui/prompts.ts +101 -0
- package/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 louisraetz
|
|
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,154 @@
|
|
|
1
|
+
# steamidled 🎮💤
|
|
2
|
+
|
|
3
|
+
Steam game idler CLI and daemon. Rack up playtime while you touch grass. Run up to 32 games simultaneously, collect trading cards, and finally look like you have no life (in a good way).
|
|
4
|
+
|
|
5
|
+
## ✨ Why does this exist?
|
|
6
|
+
|
|
7
|
+
This is a hobby project, born from a weekend of vibe coding and questionable life choices. I wanted more hours in games I'll never actually play, and here we are.
|
|
8
|
+
|
|
9
|
+
Built with love, caffeine, and Claude. No regrets. 🤖☕
|
|
10
|
+
|
|
11
|
+
## 🚀 Features
|
|
12
|
+
|
|
13
|
+
- **📱 QR Code Login** — Scan with Steam mobile app
|
|
14
|
+
- **🔐 Traditional Login** — Username/password with Steam Guard (for the old school folks)
|
|
15
|
+
- **🎯 Idle up to 32 games** — Because Steam said that's the limit and who are we to argue
|
|
16
|
+
- **📊 Real-time tracking** — Watch numbers go up. Dopamine achieved.
|
|
17
|
+
- **⭐ Favorites system** — Star your favorites so you can pretend you're organized
|
|
18
|
+
- **⏸️ Auto-pause** — Automatically pauses when you actually play a game (rare occurrence)
|
|
19
|
+
- **💾 Persistent sessions** — Remembers your login so you don't have to
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
### npm
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @louisraetz/steamidled
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Homebrew 🍺
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
brew tap louisraetz/steamidled
|
|
33
|
+
brew install steamidled
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 🎮 Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
steamidled # Interactive mode
|
|
40
|
+
steamidled --headless # Headless mode (auto-start favorites)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Interactive mode
|
|
44
|
+
|
|
45
|
+
1. **Pick your login method** — QR code is chef's kiss 👨🍳💋
|
|
46
|
+
2. **Select your games** — Go wild, pick all of them, I won't judge
|
|
47
|
+
3. **Watch the hours roll in** — This is what peak productivity looks like
|
|
48
|
+
|
|
49
|
+
### Headless mode
|
|
50
|
+
|
|
51
|
+
Automatically logs in and starts idling your favorited games. Perfect for running as a service. Requires running interactively first to log in and set up favorites.
|
|
52
|
+
|
|
53
|
+
## ⌨️ Controls
|
|
54
|
+
|
|
55
|
+
### Game Selection
|
|
56
|
+
|
|
57
|
+
| Key | What it does |
|
|
58
|
+
|-----|--------------|
|
|
59
|
+
| `↑` `↓` | Navigate (you got this) |
|
|
60
|
+
| `Space` | Toggle game on/off |
|
|
61
|
+
| `F` | Favorite a game ⭐ |
|
|
62
|
+
| `S` | Start all favorites |
|
|
63
|
+
| `Enter` | Let's gooo 🚀 |
|
|
64
|
+
|
|
65
|
+
### While Idling
|
|
66
|
+
|
|
67
|
+
| Key | What it does |
|
|
68
|
+
|-----|--------------|
|
|
69
|
+
| `E` | Edit your selection (changed your mind?) |
|
|
70
|
+
| `Q` | Quit gracefully (like a gentleman) |
|
|
71
|
+
| `Ctrl+C` | Rage quit |
|
|
72
|
+
|
|
73
|
+
## 🐧 Running 24/7 on Linux
|
|
74
|
+
|
|
75
|
+
Want to idle games while you sleep? Same. Run it as a systemd service with `--headless` mode.
|
|
76
|
+
|
|
77
|
+
### Prerequisites
|
|
78
|
+
|
|
79
|
+
1. Run the tool interactively once to log in and set up favorites
|
|
80
|
+
2. Find where it lives: `which steamidled`
|
|
81
|
+
|
|
82
|
+
### Create the Service
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
sudo nano /etc/systemd/system/steamidled.service
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ini
|
|
89
|
+
[Unit]
|
|
90
|
+
Description=steamidled
|
|
91
|
+
After=network-online.target
|
|
92
|
+
Wants=network-online.target
|
|
93
|
+
|
|
94
|
+
[Service]
|
|
95
|
+
Type=simple
|
|
96
|
+
User=YOUR_USERNAME
|
|
97
|
+
ExecStart=/usr/bin/steamidled --headless
|
|
98
|
+
Restart=on-failure
|
|
99
|
+
RestartSec=10
|
|
100
|
+
|
|
101
|
+
[Install]
|
|
102
|
+
WantedBy=multi-user.target
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Replace `YOUR_USERNAME` with your username.
|
|
106
|
+
|
|
107
|
+
### Fire it up
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
sudo systemctl daemon-reload
|
|
111
|
+
sudo systemctl enable steamidled
|
|
112
|
+
sudo systemctl start steamidled
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Useful commands
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
sudo systemctl status steamidled # Is it alive?
|
|
119
|
+
journalctl -u steamidled -f # What's it thinking?
|
|
120
|
+
sudo systemctl stop steamidled # Take a break
|
|
121
|
+
sudo systemctl restart steamidled # Turn it off and on again
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 📁 Where's my stuff?
|
|
125
|
+
|
|
126
|
+
Everything lives in `~/.steam-idler/`:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
~/.steam-idler/
|
|
130
|
+
├── credentials.json # Your login token (keep it secret 🤫)
|
|
131
|
+
└── favorites-{username}.json # Your favorite games
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 📋 Requirements
|
|
135
|
+
|
|
136
|
+
- Node.js 18+ (we're modern here)
|
|
137
|
+
- A Steam account with games (shocking, I know)
|
|
138
|
+
- Steam mobile app (optional, but makes login ✨fancy✨)
|
|
139
|
+
|
|
140
|
+
## 🤝 Contributing
|
|
141
|
+
|
|
142
|
+
Found a bug? Have an idea? PRs welcome! This is a hobby project so I might be slow, but I appreciate you. 💙
|
|
143
|
+
|
|
144
|
+
## ⚠️ Disclaimer
|
|
145
|
+
|
|
146
|
+
This is just a fun side project. Use responsibly. I'm not responsible if Valve gets mad at you or whatever. Probably don't idle 10,000 hours in a game you've never launched. Or do. I'm not your mom.
|
|
147
|
+
|
|
148
|
+
## 📄 License
|
|
149
|
+
|
|
150
|
+
MIT — Do whatever you want with it ✌️
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
*Made with mass vibe coding energy* 🌊✨
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as readline from 'node:readline';
|
|
3
|
+
import { SteamClient } from './steam/client.js';
|
|
4
|
+
import { loginWithQR, loginWithCredentials, loginWithStoredCredentials, hasStoredLogin, } from './steam/auth.js';
|
|
5
|
+
import { promptAuthMethod } from './ui/prompts.js';
|
|
6
|
+
import { selectGames } from './ui/gameSelector.js';
|
|
7
|
+
import { showWelcome, showIdlingStatus, showLoginSuccess, showLoginError, showGoodbye, showError, } from './ui/display.js';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
const client = new SteamClient();
|
|
10
|
+
let isIdling = false;
|
|
11
|
+
let idlingGames = [];
|
|
12
|
+
let pausedGames = [];
|
|
13
|
+
let allGames = [];
|
|
14
|
+
let startTime = null;
|
|
15
|
+
let updateInterval = null;
|
|
16
|
+
let isInEditMode = false;
|
|
17
|
+
let accountName = '';
|
|
18
|
+
// Main entry point that orchestrates the application flow
|
|
19
|
+
async function main() {
|
|
20
|
+
showWelcome();
|
|
21
|
+
process.on('SIGINT', handleShutdown);
|
|
22
|
+
process.on('SIGTERM', handleShutdown);
|
|
23
|
+
const loginResult = await handleLogin();
|
|
24
|
+
if (!loginResult.success) {
|
|
25
|
+
showLoginError(loginResult.error || 'Unknown error');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
accountName = loginResult.accountName;
|
|
29
|
+
showLoginSuccess(accountName);
|
|
30
|
+
const spinner = ora('Fetching your game library...').start();
|
|
31
|
+
try {
|
|
32
|
+
allGames = await client.getOwnedGames();
|
|
33
|
+
spinner.succeed(`Found ${allGames.length} games in your library`);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
spinner.fail('Failed to fetch games');
|
|
37
|
+
showError(err.message);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
if (allGames.length === 0) {
|
|
41
|
+
showError('No games found in your library');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const { selectedGames, quickStart } = await selectGames(allGames, accountName);
|
|
45
|
+
if (selectedGames.length === 0) {
|
|
46
|
+
showError('No games selected');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
startIdling(selectedGames);
|
|
50
|
+
await new Promise(() => {
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Handles authentication method selection and login
|
|
54
|
+
async function handleLogin() {
|
|
55
|
+
const hasStored = hasStoredLogin();
|
|
56
|
+
const method = await promptAuthMethod(hasStored);
|
|
57
|
+
switch (method) {
|
|
58
|
+
case 'stored':
|
|
59
|
+
return loginWithStoredCredentials(client);
|
|
60
|
+
case 'qr':
|
|
61
|
+
return loginWithQR(client);
|
|
62
|
+
case 'credentials':
|
|
63
|
+
return loginWithCredentials(client);
|
|
64
|
+
default:
|
|
65
|
+
return { success: false, error: 'Invalid login method' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Sets up raw mode keyboard input handling for edit and quit commands
|
|
69
|
+
function setupKeyboardInput() {
|
|
70
|
+
if (process.stdin.isTTY) {
|
|
71
|
+
readline.emitKeypressEvents(process.stdin);
|
|
72
|
+
process.stdin.setRawMode(true);
|
|
73
|
+
process.stdin.resume();
|
|
74
|
+
process.stdin.on('keypress', async (str, key) => {
|
|
75
|
+
if (key && key.ctrl && key.name === 'c') {
|
|
76
|
+
handleShutdown();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (isInEditMode) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (str === 'e' || str === 'E') {
|
|
83
|
+
await enterEditMode();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (str === 'q' || str === 'Q') {
|
|
87
|
+
handleShutdown();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Creates an IdlingGame object from a GameSelection
|
|
94
|
+
function createIdlingGame(game, steamGame) {
|
|
95
|
+
const initialPlaytime = steamGame?.playtime_forever ?? 0;
|
|
96
|
+
return {
|
|
97
|
+
appid: game.appid,
|
|
98
|
+
name: game.name,
|
|
99
|
+
initialPlaytime,
|
|
100
|
+
startTime: new Date(),
|
|
101
|
+
accumulatedMs: 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Converts IdlingGame array back to GameSelection array for the selector
|
|
105
|
+
function getGameCurrentSelection() {
|
|
106
|
+
return [...idlingGames, ...pausedGames].map((g) => ({
|
|
107
|
+
appid: g.appid,
|
|
108
|
+
name: g.name,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
// Handles edit mode for changing game selection while idling
|
|
112
|
+
async function enterEditMode() {
|
|
113
|
+
isInEditMode = true;
|
|
114
|
+
if (updateInterval) {
|
|
115
|
+
clearInterval(updateInterval);
|
|
116
|
+
updateInterval = null;
|
|
117
|
+
}
|
|
118
|
+
const now = new Date();
|
|
119
|
+
for (const game of idlingGames) {
|
|
120
|
+
game.accumulatedMs += now.getTime() - game.startTime.getTime();
|
|
121
|
+
}
|
|
122
|
+
console.clear();
|
|
123
|
+
const currentSelection = getGameCurrentSelection();
|
|
124
|
+
const { selectedGames: newSelection } = await selectGames(allGames, accountName, currentSelection);
|
|
125
|
+
if (newSelection.length === 0) {
|
|
126
|
+
showError('No games selected - keeping current selection');
|
|
127
|
+
for (const game of idlingGames) {
|
|
128
|
+
game.startTime = new Date();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const oldAppIds = new Set([...idlingGames, ...pausedGames].map((g) => g.appid));
|
|
133
|
+
const newAppIds = new Set(newSelection.map((g) => g.appid));
|
|
134
|
+
const keptIdlingGames = [];
|
|
135
|
+
const keptPausedGames = [];
|
|
136
|
+
for (const game of idlingGames) {
|
|
137
|
+
if (newAppIds.has(game.appid)) {
|
|
138
|
+
game.startTime = new Date();
|
|
139
|
+
keptIdlingGames.push(game);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const game of pausedGames) {
|
|
143
|
+
if (newAppIds.has(game.appid)) {
|
|
144
|
+
keptPausedGames.push(game);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const selection of newSelection) {
|
|
148
|
+
if (!oldAppIds.has(selection.appid)) {
|
|
149
|
+
const steamGame = allGames.find((g) => g.appid === selection.appid);
|
|
150
|
+
keptIdlingGames.push(createIdlingGame(selection, steamGame));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
idlingGames = keptIdlingGames;
|
|
154
|
+
pausedGames = keptPausedGames;
|
|
155
|
+
const appIds = idlingGames.map((g) => g.appid);
|
|
156
|
+
client.setGamesPlaying(appIds);
|
|
157
|
+
}
|
|
158
|
+
if (process.stdin.isTTY) {
|
|
159
|
+
readline.emitKeypressEvents(process.stdin);
|
|
160
|
+
process.stdin.setRawMode(true);
|
|
161
|
+
process.stdin.resume();
|
|
162
|
+
}
|
|
163
|
+
isInEditMode = false;
|
|
164
|
+
if (startTime) {
|
|
165
|
+
showIdlingStatus(idlingGames, pausedGames);
|
|
166
|
+
updateInterval = setInterval(() => {
|
|
167
|
+
if (startTime && !isInEditMode) {
|
|
168
|
+
showIdlingStatus(idlingGames, pausedGames);
|
|
169
|
+
}
|
|
170
|
+
}, 60000);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Handles state changes when user plays or stops playing a game on Steam
|
|
174
|
+
function handlePlayingState(blocked, playingApp) {
|
|
175
|
+
if (isInEditMode)
|
|
176
|
+
return;
|
|
177
|
+
if (blocked) {
|
|
178
|
+
const gameIndex = idlingGames.findIndex((g) => g.appid === playingApp);
|
|
179
|
+
if (gameIndex !== -1) {
|
|
180
|
+
const game = idlingGames[gameIndex];
|
|
181
|
+
const now = new Date();
|
|
182
|
+
game.accumulatedMs += now.getTime() - game.startTime.getTime();
|
|
183
|
+
const [pausedGame] = idlingGames.splice(gameIndex, 1);
|
|
184
|
+
pausedGames.push(pausedGame);
|
|
185
|
+
const appIds = idlingGames.map((g) => g.appid);
|
|
186
|
+
client.setGamesPlaying(appIds);
|
|
187
|
+
showIdlingStatus(idlingGames, pausedGames);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
if (pausedGames.length > 0) {
|
|
192
|
+
const now = new Date();
|
|
193
|
+
for (const game of pausedGames) {
|
|
194
|
+
game.startTime = now;
|
|
195
|
+
}
|
|
196
|
+
idlingGames.push(...pausedGames);
|
|
197
|
+
pausedGames = [];
|
|
198
|
+
const appIds = idlingGames.map((g) => g.appid);
|
|
199
|
+
client.setGamesPlaying(appIds);
|
|
200
|
+
showIdlingStatus(idlingGames, pausedGames);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Initializes the idling session and starts the display loop
|
|
205
|
+
function startIdling(games) {
|
|
206
|
+
isIdling = true;
|
|
207
|
+
startTime = new Date();
|
|
208
|
+
idlingGames = games.map((game) => {
|
|
209
|
+
const steamGame = allGames.find((g) => g.appid === game.appid);
|
|
210
|
+
return createIdlingGame(game, steamGame);
|
|
211
|
+
});
|
|
212
|
+
pausedGames = [];
|
|
213
|
+
const appIds = games.map((g) => g.appid);
|
|
214
|
+
client.setGamesPlaying(appIds);
|
|
215
|
+
setupKeyboardInput();
|
|
216
|
+
showIdlingStatus(idlingGames, pausedGames);
|
|
217
|
+
updateInterval = setInterval(() => {
|
|
218
|
+
if (startTime && !isInEditMode) {
|
|
219
|
+
showIdlingStatus(idlingGames, pausedGames);
|
|
220
|
+
}
|
|
221
|
+
}, 60000);
|
|
222
|
+
client.onPlayingState(handlePlayingState);
|
|
223
|
+
client.onDisconnected((eresult, msg) => {
|
|
224
|
+
console.log(`\nDisconnected from Steam: ${msg} (${eresult})`);
|
|
225
|
+
handleShutdown();
|
|
226
|
+
});
|
|
227
|
+
client.onError((err) => {
|
|
228
|
+
console.log(`\nSteam error: ${err.message}`);
|
|
229
|
+
handleShutdown();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// Graceful shutdown handler that saves state and logs out
|
|
233
|
+
function handleShutdown() {
|
|
234
|
+
console.log('\n');
|
|
235
|
+
if (updateInterval) {
|
|
236
|
+
clearInterval(updateInterval);
|
|
237
|
+
updateInterval = null;
|
|
238
|
+
}
|
|
239
|
+
if (isIdling) {
|
|
240
|
+
client.stopPlaying();
|
|
241
|
+
isIdling = false;
|
|
242
|
+
}
|
|
243
|
+
if (client.isLoggedIn) {
|
|
244
|
+
client.logout();
|
|
245
|
+
}
|
|
246
|
+
showGoodbye();
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
main().catch((err) => {
|
|
250
|
+
showError(err.message);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SteamClient } from './client.js';
|
|
2
|
+
import { clearCredentials } from '../storage/credentials.js';
|
|
3
|
+
import type { LoginResult } from '../types/index.js';
|
|
4
|
+
export declare function loginWithQR(client: SteamClient): Promise<LoginResult>;
|
|
5
|
+
export declare function loginWithCredentials(client: SteamClient): Promise<LoginResult>;
|
|
6
|
+
export declare function loginWithStoredCredentials(client: SteamClient): Promise<LoginResult>;
|
|
7
|
+
export declare function hasStoredLogin(): boolean;
|
|
8
|
+
export { clearCredentials };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { LoginSession, EAuthTokenPlatformType, EAuthSessionGuardType } from 'steam-session';
|
|
2
|
+
import qrcode from 'qrcode-terminal';
|
|
3
|
+
import { input, password as passwordPrompt } from '@inquirer/prompts';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { saveCredentials, loadCredentials, clearCredentials, hasStoredCredentials, } from '../storage/credentials.js';
|
|
7
|
+
// Authenticates via QR code scanned with the Steam mobile app
|
|
8
|
+
export async function loginWithQR(client) {
|
|
9
|
+
const session = new LoginSession(EAuthTokenPlatformType.SteamClient);
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
session.on('authenticated', async () => {
|
|
12
|
+
const refreshToken = session.refreshToken;
|
|
13
|
+
const accountName = session.accountName;
|
|
14
|
+
if (!refreshToken || !accountName) {
|
|
15
|
+
resolve({ success: false, error: 'Failed to get credentials from QR login' });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
saveCredentials({
|
|
19
|
+
accountName,
|
|
20
|
+
refreshToken,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
await client.loginWithRefreshToken(refreshToken, accountName);
|
|
24
|
+
resolve({ success: true, accountName });
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
resolve({ success: false, error: err.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
session.on('timeout', () => {
|
|
31
|
+
resolve({ success: false, error: 'QR code timed out' });
|
|
32
|
+
});
|
|
33
|
+
session.on('error', (err) => {
|
|
34
|
+
resolve({ success: false, error: err.message });
|
|
35
|
+
});
|
|
36
|
+
session.startWithQR().then((result) => {
|
|
37
|
+
if (result.qrChallengeUrl) {
|
|
38
|
+
console.log(chalk.cyan('\nScan this QR code with your Steam mobile app:\n'));
|
|
39
|
+
qrcode.generate(result.qrChallengeUrl);
|
|
40
|
+
console.log(chalk.gray('\nWaiting for approval...\n'));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Authenticates via username and password with Steam Guard support
|
|
46
|
+
export async function loginWithCredentials(client) {
|
|
47
|
+
const session = new LoginSession(EAuthTokenPlatformType.SteamClient);
|
|
48
|
+
const accountName = await input({
|
|
49
|
+
message: 'Steam username:',
|
|
50
|
+
});
|
|
51
|
+
const password = await passwordPrompt({
|
|
52
|
+
message: 'Steam password:',
|
|
53
|
+
mask: '*',
|
|
54
|
+
});
|
|
55
|
+
const spinner = ora('Logging in...').start();
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
session.on('authenticated', async () => {
|
|
58
|
+
spinner.stop();
|
|
59
|
+
const refreshToken = session.refreshToken;
|
|
60
|
+
if (!refreshToken) {
|
|
61
|
+
resolve({ success: false, error: 'Failed to get refresh token' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
saveCredentials({
|
|
65
|
+
accountName,
|
|
66
|
+
refreshToken,
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
await client.loginWithRefreshToken(refreshToken, accountName);
|
|
70
|
+
resolve({ success: true, accountName });
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
resolve({ success: false, error: err.message });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
session.on('timeout', () => {
|
|
77
|
+
spinner.stop();
|
|
78
|
+
resolve({ success: false, error: 'Login timed out' });
|
|
79
|
+
});
|
|
80
|
+
session.on('error', (err) => {
|
|
81
|
+
spinner.stop();
|
|
82
|
+
resolve({ success: false, error: err.message });
|
|
83
|
+
});
|
|
84
|
+
session.on('steamGuardMachineToken', () => {
|
|
85
|
+
});
|
|
86
|
+
session.startWithCredentials({ accountName, password }).then(async (result) => {
|
|
87
|
+
if (result.actionRequired) {
|
|
88
|
+
spinner.stop();
|
|
89
|
+
for (const action of result.validActions || []) {
|
|
90
|
+
if (action.type === EAuthSessionGuardType.EmailCode ||
|
|
91
|
+
action.type === EAuthSessionGuardType.DeviceCode) {
|
|
92
|
+
const guardType = action.type === EAuthSessionGuardType.EmailCode ? 'email' : 'mobile app';
|
|
93
|
+
console.log(chalk.yellow(`\nSteam Guard code required (${guardType})`));
|
|
94
|
+
const code = await input({
|
|
95
|
+
message: 'Enter Steam Guard code:',
|
|
96
|
+
});
|
|
97
|
+
spinner.start('Verifying code...');
|
|
98
|
+
try {
|
|
99
|
+
await session.submitSteamGuardCode(code);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
spinner.stop();
|
|
103
|
+
resolve({ success: false, error: err.message });
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (action.type === EAuthSessionGuardType.DeviceConfirmation) {
|
|
108
|
+
console.log(chalk.yellow('\nPlease confirm the login request on your Steam mobile app...'));
|
|
109
|
+
spinner.start('Waiting for confirmation...');
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}).catch((err) => {
|
|
115
|
+
spinner.stop();
|
|
116
|
+
resolve({ success: false, error: err.message });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Logs in using a previously saved refresh token
|
|
121
|
+
export async function loginWithStoredCredentials(client) {
|
|
122
|
+
const credentials = loadCredentials();
|
|
123
|
+
if (!credentials) {
|
|
124
|
+
return { success: false, error: 'No stored credentials' };
|
|
125
|
+
}
|
|
126
|
+
const spinner = ora(`Logging in as ${chalk.cyan(credentials.accountName)}...`).start();
|
|
127
|
+
try {
|
|
128
|
+
await client.loginWithRefreshToken(credentials.refreshToken, credentials.accountName);
|
|
129
|
+
spinner.succeed(`Logged in as ${chalk.cyan(credentials.accountName)}`);
|
|
130
|
+
return { success: true, accountName: credentials.accountName };
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
spinner.fail('Stored credentials expired');
|
|
134
|
+
clearCredentials();
|
|
135
|
+
return { success: false, error: 'Stored credentials expired' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Checks if stored login credentials exist
|
|
139
|
+
export function hasStoredLogin() {
|
|
140
|
+
return hasStoredCredentials();
|
|
141
|
+
}
|
|
142
|
+
export { clearCredentials };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import SteamUser from 'steam-user';
|
|
2
|
+
import type { SteamGame } from '../types/index.js';
|
|
3
|
+
export declare class SteamClient {
|
|
4
|
+
private client;
|
|
5
|
+
private _isLoggedIn;
|
|
6
|
+
private _accountName;
|
|
7
|
+
constructor();
|
|
8
|
+
get isLoggedIn(): boolean;
|
|
9
|
+
get accountName(): string | null;
|
|
10
|
+
get steamUser(): SteamUser;
|
|
11
|
+
loginWithRefreshToken(refreshToken: string, accountName?: string): Promise<void>;
|
|
12
|
+
loginWithCredentials(accountName: string, password: string): Promise<{
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
}>;
|
|
15
|
+
getOwnedGames(): Promise<SteamGame[]>;
|
|
16
|
+
setGamesPlaying(appIds: number[]): void;
|
|
17
|
+
stopPlaying(): void;
|
|
18
|
+
logout(): void;
|
|
19
|
+
onDisconnected(callback: (eresult: number, msg: string) => void): void;
|
|
20
|
+
onError(callback: (err: Error) => void): void;
|
|
21
|
+
onPlayingState(callback: (blocked: boolean, playingApp: number) => void): void;
|
|
22
|
+
}
|