oc-codex-multi-account 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/README.md +321 -0
- package/dist/auth-sync.d.ts +3 -0
- package/dist/auth-sync.d.ts.map +1 -0
- package/dist/auth-sync.js +105 -0
- package/dist/auth-sync.js.map +1 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +236 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +160 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-auth.d.ts +28 -0
- package/dist/codex-auth.d.ts.map +1 -0
- package/dist/codex-auth.js +174 -0
- package/dist/codex-auth.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +730 -0
- package/dist/index.js.map +1 -0
- package/dist/limits-refresh.d.ts +9 -0
- package/dist/limits-refresh.d.ts.map +1 -0
- package/dist/limits-refresh.js +48 -0
- package/dist/limits-refresh.js.map +1 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +52 -0
- package/dist/logger.js.map +1 -0
- package/dist/models.d.ts +7 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +121 -0
- package/dist/models.js.map +1 -0
- package/dist/probe-limits.d.ts +10 -0
- package/dist/probe-limits.d.ts.map +1 -0
- package/dist/probe-limits.js +160 -0
- package/dist/probe-limits.js.map +1 -0
- package/dist/rate-limits.d.ts +6 -0
- package/dist/rate-limits.d.ts.map +1 -0
- package/dist/rate-limits.js +117 -0
- package/dist/rate-limits.js.map +1 -0
- package/dist/refresh-queue.d.ts +18 -0
- package/dist/refresh-queue.d.ts.map +1 -0
- package/dist/refresh-queue.js +78 -0
- package/dist/refresh-queue.js.map +1 -0
- package/dist/rotation.d.ts +20 -0
- package/dist/rotation.d.ts.map +1 -0
- package/dist/rotation.js +273 -0
- package/dist/rotation.js.map +1 -0
- package/dist/sessions-limits.d.ts +11 -0
- package/dist/sessions-limits.d.ts.map +1 -0
- package/dist/sessions-limits.js +123 -0
- package/dist/sessions-limits.js.map +1 -0
- package/dist/store.d.ts +23 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +339 -0
- package/dist/store.js.map +1 -0
- package/dist/systemd.d.ts +10 -0
- package/dist/systemd.d.ts.map +1 -0
- package/dist/systemd.js +53 -0
- package/dist/systemd.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/web.d.ts +6 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +1857 -0
- package/dist/web.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# oc-codex-multi-account
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/oc-codex-multi-account)
|
|
4
|
+
|
|
5
|
+
Multi-account OAuth rotation for OpenAI Codex with sticky threshold switching.
|
|
6
|
+
|
|
7
|
+
> **Based on [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) by [@nummanali](https://x.com/nummanali)**. Forked and modified to add multi-account rotation support.
|
|
8
|
+
|
|
9
|
+
## Patched Build (Codex Backend Compatible)
|
|
10
|
+
|
|
11
|
+
This fork patches the plugin to talk to **ChatGPT Codex backend** (`chatgpt.com/backend-api`) with the same headers and request shape as the official Codex OAuth plugin.
|
|
12
|
+
|
|
13
|
+
**Install from npm (recommended):**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add oc-codex-multi-account --cwd ~/.config/opencode
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then set the plugin entry in `~/.config/opencode/opencode.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"plugin": ["oc-codex-multi-account@latest"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If you already installed an older build, re-run the GitHub install command above to override it.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### Via npm (Recommended)
|
|
32
|
+
|
|
33
|
+
Add to your `~/.config/opencode/opencode.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"plugin": ["oc-codex-multi-account@latest"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
OpenCode will auto-install on first run.
|
|
42
|
+
|
|
43
|
+
### Manual Install
|
|
44
|
+
|
|
45
|
+
If auto-install fails, install manually:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bun add oc-codex-multi-account --cwd ~/.config/opencode
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### From Source
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/gaboe/oc-codex-multi-account.git
|
|
55
|
+
cd oc-codex-multi-account
|
|
56
|
+
bun install
|
|
57
|
+
bun run build
|
|
58
|
+
bun link
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Add Your Accounts
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Add each account (opens browser for OAuth)
|
|
65
|
+
opencode-multi-auth add personal
|
|
66
|
+
opencode-multi-auth add work
|
|
67
|
+
opencode-multi-auth add backup
|
|
68
|
+
|
|
69
|
+
# Each command opens your browser - log in with a different ChatGPT account each time
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Verify Setup
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
opencode-multi-auth status
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Output:
|
|
79
|
+
```
|
|
80
|
+
[multi-auth] Account Status
|
|
81
|
+
|
|
82
|
+
Strategy: round-robin
|
|
83
|
+
Accounts: 3
|
|
84
|
+
Active: personal
|
|
85
|
+
|
|
86
|
+
personal (active)
|
|
87
|
+
Email: you@personal.com
|
|
88
|
+
Uses: 12
|
|
89
|
+
Token expires: 12/25/2025, 3:00:00 PM
|
|
90
|
+
|
|
91
|
+
work
|
|
92
|
+
Email: you@work.com
|
|
93
|
+
Uses: 10
|
|
94
|
+
Token expires: 12/25/2025, 3:00:00 PM
|
|
95
|
+
|
|
96
|
+
backup
|
|
97
|
+
Email: you@backup.com
|
|
98
|
+
Uses: 8
|
|
99
|
+
Token expires: 12/25/2025, 3:00:00 PM
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Web Dashboard (Local Only)
|
|
103
|
+
|
|
104
|
+
Launch the local dashboard:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
opencode-multi-auth web --port 3434 --host 127.0.0.1
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or from the repo:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm run web
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Open `http://127.0.0.1:3434` to manage Codex CLI tokens from `~/.codex/auth.json`:
|
|
117
|
+
- Sync current auth.json token into your local list
|
|
118
|
+
- See which token is active on the device
|
|
119
|
+
- Switch auth.json to a stored token
|
|
120
|
+
- Refresh OAuth tokens (per-token or all)
|
|
121
|
+
- Refresh 5-hour and weekly limits manually (probe-run per alias)
|
|
122
|
+
- Search/filter by alias/email/tags/notes
|
|
123
|
+
- Sort by remaining limits, expiry, or alias; recommended token badge
|
|
124
|
+
- Tag and annotate tokens (notes)
|
|
125
|
+
- Queue-based refresh with progress + stop
|
|
126
|
+
- Limit history sparklines and trend rate
|
|
127
|
+
- Built-in log view
|
|
128
|
+
|
|
129
|
+
The dashboard watches `~/.codex/auth.json` and will add new tokens as you log in via Codex CLI.
|
|
130
|
+
|
|
131
|
+
Limit refresh runs `codex exec` in a per-alias sandbox (`~/.codex-multi/<alias>`) so you can
|
|
132
|
+
update limits for any stored token without switching the active device token.
|
|
133
|
+
|
|
134
|
+
### Optional Store Encryption
|
|
135
|
+
|
|
136
|
+
Set `CODEX_SOFT_STORE_PASSPHRASE` to encrypt `~/.config/opencode-multi-auth/accounts.json` at rest:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
export CODEX_SOFT_STORE_PASSPHRASE="your-passphrase"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
If the store is encrypted and the passphrase is missing, the UI will show a locked status and refuse to overwrite.
|
|
143
|
+
|
|
144
|
+
### Systemd Autostart (user service)
|
|
145
|
+
|
|
146
|
+
Install and enable the user service:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
opencode-multi-auth service install --port 3434 --host 127.0.0.1
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Check status or disable:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
opencode-multi-auth service status
|
|
156
|
+
opencode-multi-auth service disable
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Logs
|
|
160
|
+
|
|
161
|
+
The dashboard writes logs to `~/.config/opencode-multi-auth/logs/codex-soft.log` by default.
|
|
162
|
+
Override with `CODEX_SOFT_LOG_PATH` if you want a custom path.
|
|
163
|
+
|
|
164
|
+
## Configure OpenCode
|
|
165
|
+
|
|
166
|
+
Add to your `~/.config/opencode/opencode.json`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"plugin": ["oc-codex-multi-account@latest"]
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Or with other plugins:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"plugin": [
|
|
179
|
+
"oh-my-opencode",
|
|
180
|
+
"oc-codex-multi-account@latest"
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## Background Notifications (macOS)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
### iPhone notifications via ntfy (click to open session)
|
|
190
|
+
|
|
191
|
+
If you want push notifications on iOS (with a clickable link to the OpenCode web session), use `ntfy`.
|
|
192
|
+
|
|
193
|
+
1) Install the **ntfy** app on iPhone and subscribe to a topic.
|
|
194
|
+
|
|
195
|
+
2) Set these env vars on the Mac where OpenCode runs:
|
|
196
|
+
|
|
197
|
+
- `OPENCODE_MULTI_AUTH_NOTIFY_NTFY_URL`
|
|
198
|
+
Example: `https://ntfy.sh/<your-topic>` (or your self-hosted ntfy URL)
|
|
199
|
+
- `OPENCODE_MULTI_AUTH_NOTIFY_UI_BASE_URL`
|
|
200
|
+
Base URL of your OpenCode web UI reachable from iPhone.
|
|
201
|
+
Example (Tailscale): `http://100.x.y.z:4096`
|
|
202
|
+
- Optional: `OPENCODE_MULTI_AUTH_NOTIFY_NTFY_TOKEN` (Bearer token)
|
|
203
|
+
|
|
204
|
+
The plugin sends notifications for:
|
|
205
|
+
|
|
206
|
+
- `session.idle` (finished): priority `3`
|
|
207
|
+
- `session.status` with `retry`: priority `4`
|
|
208
|
+
- `session.error`: priority `5`
|
|
209
|
+
|
|
210
|
+
When possible, the notification body includes `Project` + session `Title`, plus the `sessionID`.
|
|
211
|
+
It also attaches a `Click:` URL like `<base>/session/<sessionID>` so tapping the push opens the session.
|
|
212
|
+
|
|
213
|
+
This plugin can send a **macOS notification + sound** when a session finishes work.
|
|
214
|
+
It listens for OpenCode events (`session.status` and `session.idle`).
|
|
215
|
+
|
|
216
|
+
Defaults:
|
|
217
|
+
- Enabled by default
|
|
218
|
+
- Sound: `/System/Library/Sounds/Glass.aiff`
|
|
219
|
+
|
|
220
|
+
Environment variables:
|
|
221
|
+
- `OPENCODE_MULTI_AUTH_NOTIFY=0` disables notifications
|
|
222
|
+
- `OPENCODE_MULTI_AUTH_NOTIFY_SOUND=/path/to/sound.aiff` overrides the sound
|
|
223
|
+
- `OPENCODE_MULTI_AUTH_NOTIFY_MAC_OPEN=0` disables click-to-open on macOS (when available)
|
|
224
|
+
|
|
225
|
+
Clickable macOS notifications require `terminal-notifier` (optional). If installed, clicking the banner opens the session URL.
|
|
226
|
+
|
|
227
|
+
If OpenCode seems to only make progress when the window is focused, macOS may be throttling it.
|
|
228
|
+
Try disabling App Nap for OpenCode.app (Finder -> Get Info -> Prevent App Nap),
|
|
229
|
+
or run the server from a terminal under `caffeinate`.
|
|
230
|
+
|
|
231
|
+
## Codex Latest Model Mapping
|
|
232
|
+
|
|
233
|
+
OpenCode may not list the newest Codex model yet (it keeps an internal allowlist).
|
|
234
|
+
This plugin can still use the newest model by **mapping** the selected Codex model
|
|
235
|
+
to the latest backend model on ChatGPT.
|
|
236
|
+
|
|
237
|
+
Default behavior:
|
|
238
|
+
- If you select `openai/gpt-5.2-codex` (or `openai/gpt-5-codex`), the plugin will send requests as `gpt-5.3-codex`.
|
|
239
|
+
|
|
240
|
+
Environment variables:
|
|
241
|
+
- `OPENCODE_MULTI_AUTH_PREFER_CODEX_LATEST=0` disables the mapping (use exact model).
|
|
242
|
+
- `OPENCODE_MULTI_AUTH_CODEX_LATEST_MODEL=gpt-5.3-codex` overrides the target model.
|
|
243
|
+
- `OPENCODE_MULTI_AUTH_DEBUG=1` prints mapping logs like: `model map: gpt-5.2-codex -> gpt-5.3-codex`.
|
|
244
|
+
|
|
245
|
+
## Troubleshooting
|
|
246
|
+
|
|
247
|
+
### BunInstallFailedError (DependencyLoop)
|
|
248
|
+
|
|
249
|
+
If OpenCode fails to boot with:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
BunInstallFailedError
|
|
253
|
+
{ "pkg": "oc-codex-multi-account", "version": "latest" }
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
It usually means an older `@a3fckx/opencode-multi-auth` dependency is still present.
|
|
257
|
+
|
|
258
|
+
Fix:
|
|
259
|
+
|
|
260
|
+
1) Remove the old dependency from `~/.config/opencode/package.json`:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"dependencies": {
|
|
265
|
+
"@a3fckx/opencode-multi-auth": "^1.0.4"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
2) Reinstall:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
bun add oc-codex-multi-account --cwd ~/.config/opencode
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Optional fallback: use a file path plugin entry if installs are blocked:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"plugin": [
|
|
281
|
+
"file:///Users/<you>/.config/opencode/node_modules/oc-codex-multi-account/dist/index.js"
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## How It Works
|
|
287
|
+
|
|
288
|
+
| Feature | Behavior |
|
|
289
|
+
|---------|----------|
|
|
290
|
+
| **Rotation** | Round-robin across all accounts per API call |
|
|
291
|
+
| **Rate Limits** | Auto-skips rate-limited account for 5 min, uses next |
|
|
292
|
+
| **Token Refresh** | Auto-refreshes tokens before expiry |
|
|
293
|
+
| **Models** | Auto-discovers GPT-5.x models from OpenAI API |
|
|
294
|
+
| **Storage** | `~/.config/opencode-multi-auth/accounts.json` |
|
|
295
|
+
|
|
296
|
+
## CLI Commands
|
|
297
|
+
|
|
298
|
+
| Command | Description |
|
|
299
|
+
|---------|-------------|
|
|
300
|
+
| `add <alias>` | Add new account via OAuth (opens browser) |
|
|
301
|
+
| `remove <alias>` | Remove an account |
|
|
302
|
+
| `list` | List all configured accounts |
|
|
303
|
+
| `status` | Detailed status with usage counts |
|
|
304
|
+
| `path` | Show config file location |
|
|
305
|
+
| `web` | Launch local Codex auth.json dashboard |
|
|
306
|
+
| `service` | Install/disable systemd user service |
|
|
307
|
+
| `help` | Show help message |
|
|
308
|
+
|
|
309
|
+
## Requirements
|
|
310
|
+
|
|
311
|
+
- ChatGPT Plus/Pro subscription(s)
|
|
312
|
+
- OpenCode CLI
|
|
313
|
+
|
|
314
|
+
## Credits
|
|
315
|
+
|
|
316
|
+
- Original OAuth implementation: [numman-ali/opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth)
|
|
317
|
+
- Multi-account rotation: [@a3fckx](https://github.com/a3fckx)
|
|
318
|
+
|
|
319
|
+
## License
|
|
320
|
+
|
|
321
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-sync.d.ts","sourceRoot":"","sources":["../src/auth-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAkD5C,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDtF"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { addAccount, loadStore, updateAccount } from './store.js';
|
|
2
|
+
import { decodeJwtPayload, getAccountIdFromClaims, getEmailFromClaims } from './codex-auth.js';
|
|
3
|
+
const OPENAI_ISSUER = 'https://auth.openai.com';
|
|
4
|
+
const AUTH_SYNC_COOLDOWN_MS = 10_000;
|
|
5
|
+
let lastSyncedAccess = null;
|
|
6
|
+
let lastSyncAt = 0;
|
|
7
|
+
async function fetchEmail(accessToken) {
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(`${OPENAI_ISSUER}/userinfo`, {
|
|
10
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
return undefined;
|
|
14
|
+
const user = (await res.json());
|
|
15
|
+
return user.email;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function findAccountAliasByToken(access, refresh) {
|
|
22
|
+
const store = loadStore();
|
|
23
|
+
for (const account of Object.values(store.accounts)) {
|
|
24
|
+
if (account.accessToken === access)
|
|
25
|
+
return account.alias;
|
|
26
|
+
if (refresh && account.refreshToken === refresh)
|
|
27
|
+
return account.alias;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function findAccountAliasByEmail(email, store) {
|
|
32
|
+
for (const account of Object.values(store.accounts)) {
|
|
33
|
+
if (account.email && account.email === email)
|
|
34
|
+
return account.alias;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function buildAlias(email, existingAliases) {
|
|
39
|
+
const base = email ? email.split('@')[0] : 'account';
|
|
40
|
+
let candidate = base || 'account';
|
|
41
|
+
let suffix = 1;
|
|
42
|
+
while (existingAliases.has(candidate)) {
|
|
43
|
+
candidate = `${base}-${suffix}`;
|
|
44
|
+
suffix += 1;
|
|
45
|
+
}
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
export async function syncAuthFromOpenCode(getAuth) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (now - lastSyncAt < AUTH_SYNC_COOLDOWN_MS)
|
|
51
|
+
return;
|
|
52
|
+
lastSyncAt = now;
|
|
53
|
+
let auth = null;
|
|
54
|
+
try {
|
|
55
|
+
auth = await getAuth();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!auth || auth.type !== 'oauth')
|
|
61
|
+
return;
|
|
62
|
+
if (!auth.access)
|
|
63
|
+
return;
|
|
64
|
+
if (auth.access === lastSyncedAccess)
|
|
65
|
+
return;
|
|
66
|
+
lastSyncedAccess = auth.access;
|
|
67
|
+
const existingAlias = findAccountAliasByToken(auth.access, auth.refresh);
|
|
68
|
+
const accessClaims = decodeJwtPayload(auth.access);
|
|
69
|
+
const derivedEmail = getEmailFromClaims(accessClaims);
|
|
70
|
+
const derivedAccountId = getAccountIdFromClaims(accessClaims);
|
|
71
|
+
if (existingAlias) {
|
|
72
|
+
updateAccount(existingAlias, {
|
|
73
|
+
accessToken: auth.access,
|
|
74
|
+
refreshToken: auth.refresh,
|
|
75
|
+
expiresAt: auth.expires,
|
|
76
|
+
email: derivedEmail,
|
|
77
|
+
accountId: derivedAccountId
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const store = loadStore();
|
|
82
|
+
const email = (await fetchEmail(auth.access)) || derivedEmail;
|
|
83
|
+
if (email) {
|
|
84
|
+
const existingByEmail = findAccountAliasByEmail(email, store);
|
|
85
|
+
if (existingByEmail) {
|
|
86
|
+
updateAccount(existingByEmail, {
|
|
87
|
+
accessToken: auth.access,
|
|
88
|
+
refreshToken: auth.refresh,
|
|
89
|
+
expiresAt: auth.expires,
|
|
90
|
+
email
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const alias = buildAlias(email, new Set(Object.keys(store.accounts)));
|
|
96
|
+
addAccount(alias, {
|
|
97
|
+
accessToken: auth.access,
|
|
98
|
+
refreshToken: auth.refresh,
|
|
99
|
+
expiresAt: auth.expires,
|
|
100
|
+
email,
|
|
101
|
+
accountId: derivedAccountId,
|
|
102
|
+
source: 'opencode'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=auth-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-sync.js","sourceRoot":"","sources":["../src/auth-sync.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAE9F,MAAM,aAAa,GAAG,yBAAyB,CAAA;AAC/C,MAAM,qBAAqB,GAAG,MAAM,CAAA;AAEpC,IAAI,gBAAgB,GAAkB,IAAI,CAAA;AAC1C,IAAI,UAAU,GAAG,CAAC,CAAA;AAElB,KAAK,UAAU,UAAU,CAAC,WAAmB;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,aAAa,WAAW,EAAE;YACnD,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE;SACpD,CAAC,CAAA;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,SAAS,CAAA;QAC7B,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAuB,CAAA;QACrD,OAAO,IAAI,CAAC,KAAK,CAAA;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAAC,MAAc,EAAE,OAAgB;IAC/D,MAAM,KAAK,GAAG,SAAS,EAAE,CAAA;IACzB,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,WAAW,KAAK,MAAM;YAAE,OAAO,OAAO,CAAC,KAAK,CAAA;QACxD,IAAI,OAAO,IAAI,OAAO,CAAC,YAAY,KAAK,OAAO;YAAE,OAAO,OAAO,CAAC,KAAK,CAAA;IACvE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,uBAAuB,CAAC,KAAa,EAAE,KAAmC;IACjF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO,OAAO,CAAC,KAAK,CAAA;IACpE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,UAAU,CAAC,KAAyB,EAAE,eAA4B;IACzE,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACpD,IAAI,SAAS,GAAG,IAAI,IAAI,SAAS,CAAA;IACjC,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,OAAO,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QACtC,SAAS,GAAG,GAAG,IAAI,IAAI,MAAM,EAAE,CAAA;QAC/B,MAAM,IAAI,CAAC,CAAA;IACb,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAA4B;IACrE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,IAAI,GAAG,GAAG,UAAU,GAAG,qBAAqB;QAAE,OAAM;IACpD,UAAU,GAAG,GAAG,CAAA;IAEhB,IAAI,IAAI,GAAgB,IAAI,CAAA;IAC5B,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,OAAO,EAAE,CAAA;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAM;IACR,CAAC;IAED,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAM;IAC1C,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAM;IACxB,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB;QAAE,OAAM;IAE5C,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAA;IAE9B,MAAM,aAAa,GAAG,uBAAuB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;IACxE,MAAM,YAAY,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAClD,MAAM,YAAY,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAA;IACrD,MAAM,gBAAgB,GAAG,sBAAsB,CAAC,YAAY,CAAC,CAAA;IAC7D,IAAI,aAAa,EAAE,CAAC;QAClB,aAAa,CAAC,aAAa,EAAE;YAC3B,WAAW,EAAE,IAAI,CAAC,MAAM;YACxB,YAAY,EAAE,IAAI,CAAC,OAAO;YAC1B,SAAS,EAAE,IAAI,CAAC,OAAO;YACvB,KAAK,EAAE,YAAY;YACnB,SAAS,EAAE,gBAAgB;SAC5B,CAAC,CAAA;QACF,OAAM;IACR,CAAC;IAED,MAAM,KAAK,GAAG,SAAS,EAAE,CAAA;IACzB,MAAM,KAAK,GAAG,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,YAAY,CAAA;IAC7D,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,eAAe,GAAG,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QAC7D,IAAI,eAAe,EAAE,CAAC;YACpB,aAAa,CAAC,eAAe,EAAE;gBAC7B,WAAW,EAAE,IAAI,CAAC,MAAM;gBACxB,YAAY,EAAE,IAAI,CAAC,OAAO;gBAC1B,SAAS,EAAE,IAAI,CAAC,OAAO;gBACvB,KAAK;aACN,CAAC,CAAA;YACF,OAAM;QACR,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;IAErE,UAAU,CAAC,KAAK,EAAE;QAChB,WAAW,EAAE,IAAI,CAAC,MAAM;QACxB,YAAY,EAAE,IAAI,CAAC,OAAO;QAC1B,SAAS,EAAE,IAAI,CAAC,OAAO;QACvB,KAAK;QACL,SAAS,EAAE,gBAAgB;QAC3B,MAAM,EAAE,UAAU;KACnB,CAAC,CAAA;AACJ,CAAC"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AccountCredentials } from './types.js';
|
|
2
|
+
interface AuthorizationFlow {
|
|
3
|
+
pkce: {
|
|
4
|
+
verifier: string;
|
|
5
|
+
challenge: string;
|
|
6
|
+
};
|
|
7
|
+
state: string;
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createAuthorizationFlow(): Promise<AuthorizationFlow>;
|
|
11
|
+
export declare function loginAccount(alias: string, flow?: AuthorizationFlow): Promise<AccountCredentials>;
|
|
12
|
+
export declare function refreshToken(alias: string): Promise<AccountCredentials | null>;
|
|
13
|
+
export declare function ensureValidToken(alias: string): Promise<string | null>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAsBpD,UAAU,iBAAiB;IACzB,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAiB1E;AAED,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CA8I7B;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA+DpF;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAe5E"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { generatePKCE } from '@openauthjs/openauth/pkce';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import * as http from 'http';
|
|
4
|
+
import * as url from 'url';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import * as os from 'node:os';
|
|
7
|
+
import { addAccount, updateAccount, loadStore } from './store.js';
|
|
8
|
+
import { clearAuthInvalid } from './rotation.js';
|
|
9
|
+
import { decodeJwtPayload, getAccountIdFromClaims, getEmailFromClaims, getExpiryFromClaims } from './codex-auth.js';
|
|
10
|
+
// OpenAI OAuth endpoints (same as official Codex CLI)
|
|
11
|
+
const OPENAI_ISSUER = 'https://auth.openai.com';
|
|
12
|
+
const AUTHORIZE_URL = `${OPENAI_ISSUER}/oauth/authorize`;
|
|
13
|
+
const TOKEN_URL = `${OPENAI_ISSUER}/oauth/token`;
|
|
14
|
+
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
15
|
+
const REDIRECT_PORT = 1455;
|
|
16
|
+
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/auth/callback`;
|
|
17
|
+
const SCOPES = ['openid', 'profile', 'email', 'offline_access'];
|
|
18
|
+
const FLOW_FILE_DIR = path.join(os.homedir(), '.config', 'opencode-multi-auth');
|
|
19
|
+
const FLOW_FILE = path.join(FLOW_FILE_DIR, 'pending-flow.json');
|
|
20
|
+
export async function createAuthorizationFlow() {
|
|
21
|
+
const pkce = await generatePKCE();
|
|
22
|
+
const state = randomBytes(16).toString('hex');
|
|
23
|
+
const authUrl = new URL(AUTHORIZE_URL);
|
|
24
|
+
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
25
|
+
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
26
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
27
|
+
authUrl.searchParams.set('scope', SCOPES.join(' '));
|
|
28
|
+
authUrl.searchParams.set('code_challenge', pkce.challenge);
|
|
29
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
30
|
+
authUrl.searchParams.set('state', state);
|
|
31
|
+
authUrl.searchParams.set('audience', 'https://api.openai.com/v1');
|
|
32
|
+
authUrl.searchParams.set('id_token_add_organizations', 'true');
|
|
33
|
+
authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
|
|
34
|
+
authUrl.searchParams.set('originator', 'codex_cli_rs');
|
|
35
|
+
return { pkce, state, url: authUrl.toString() };
|
|
36
|
+
}
|
|
37
|
+
export async function loginAccount(alias, flow) {
|
|
38
|
+
const activeFlow = flow ?? await createAuthorizationFlow();
|
|
39
|
+
const { pkce, state } = activeFlow;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let server = null;
|
|
42
|
+
const cleanup = () => {
|
|
43
|
+
if (server) {
|
|
44
|
+
server.close();
|
|
45
|
+
server = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
server = http.createServer(async (req, res) => {
|
|
49
|
+
if (!req.url?.startsWith('/auth/callback')) {
|
|
50
|
+
res.writeHead(404);
|
|
51
|
+
res.end('Not found');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const parsedUrl = url.parse(req.url, true);
|
|
55
|
+
const code = parsedUrl.query.code;
|
|
56
|
+
const returnedState = parsedUrl.query.state;
|
|
57
|
+
if (!code) {
|
|
58
|
+
res.writeHead(400);
|
|
59
|
+
res.end('No authorization code received');
|
|
60
|
+
cleanup();
|
|
61
|
+
reject(new Error('No authorization code'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (returnedState && returnedState !== state) {
|
|
65
|
+
res.writeHead(400);
|
|
66
|
+
res.end('Invalid state');
|
|
67
|
+
cleanup();
|
|
68
|
+
reject(new Error('Invalid state'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// Exchange code for tokens
|
|
73
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
76
|
+
body: new URLSearchParams({
|
|
77
|
+
grant_type: 'authorization_code',
|
|
78
|
+
client_id: CLIENT_ID,
|
|
79
|
+
code,
|
|
80
|
+
code_verifier: pkce.verifier,
|
|
81
|
+
redirect_uri: REDIRECT_URI
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
if (!tokenRes.ok) {
|
|
85
|
+
throw new Error(`Token exchange failed: ${tokenRes.status}`);
|
|
86
|
+
}
|
|
87
|
+
const tokens = (await tokenRes.json());
|
|
88
|
+
if (!tokens.refresh_token) {
|
|
89
|
+
throw new Error('Token exchange did not return a refresh_token');
|
|
90
|
+
}
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const accessClaims = decodeJwtPayload(tokens.access_token);
|
|
93
|
+
const idClaims = tokens.id_token ? decodeJwtPayload(tokens.id_token) : null;
|
|
94
|
+
const expiresAt = getExpiryFromClaims(accessClaims) || getExpiryFromClaims(idClaims) || now + tokens.expires_in * 1000;
|
|
95
|
+
let email = getEmailFromClaims(idClaims) || getEmailFromClaims(accessClaims);
|
|
96
|
+
try {
|
|
97
|
+
const userRes = await fetch(`${OPENAI_ISSUER}/userinfo`, {
|
|
98
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` }
|
|
99
|
+
});
|
|
100
|
+
if (userRes.ok) {
|
|
101
|
+
const user = (await userRes.json());
|
|
102
|
+
email = user.email || email;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* user info fetch is non-critical */
|
|
107
|
+
}
|
|
108
|
+
const accountId = getAccountIdFromClaims(idClaims) ||
|
|
109
|
+
getAccountIdFromClaims(accessClaims);
|
|
110
|
+
const store = addAccount(alias, {
|
|
111
|
+
accessToken: tokens.access_token,
|
|
112
|
+
refreshToken: tokens.refresh_token,
|
|
113
|
+
idToken: tokens.id_token,
|
|
114
|
+
accountId,
|
|
115
|
+
expiresAt,
|
|
116
|
+
email,
|
|
117
|
+
lastRefresh: new Date(now).toISOString(),
|
|
118
|
+
lastSeenAt: now,
|
|
119
|
+
source: 'opencode',
|
|
120
|
+
authInvalid: false,
|
|
121
|
+
authInvalidatedAt: undefined
|
|
122
|
+
});
|
|
123
|
+
const account = store.accounts[alias];
|
|
124
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
125
|
+
res.end(`
|
|
126
|
+
<html>
|
|
127
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
128
|
+
<h1>Account "${alias}" authenticated!</h1>
|
|
129
|
+
<p>${email || 'Unknown email'}</p>
|
|
130
|
+
<p>You can close this window.</p>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
`);
|
|
134
|
+
cleanup();
|
|
135
|
+
resolve(account);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
res.writeHead(500);
|
|
139
|
+
res.end('Authentication failed');
|
|
140
|
+
cleanup();
|
|
141
|
+
reject(err);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
server.listen(REDIRECT_PORT, () => {
|
|
145
|
+
console.log(`\n[multi-auth] Login for account "${alias}"`);
|
|
146
|
+
console.log(`[multi-auth] Open this URL in your browser:\n`);
|
|
147
|
+
console.log(` ${activeFlow.url}\n`);
|
|
148
|
+
console.log(`[multi-auth] Waiting for callback on port ${REDIRECT_PORT}...`);
|
|
149
|
+
});
|
|
150
|
+
server.on('error', (err) => {
|
|
151
|
+
if (err.code === 'EADDRINUSE') {
|
|
152
|
+
reject(new Error(`Port ${REDIRECT_PORT} is in use. Stop Codex CLI if running.`));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
reject(err);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// Timeout after 5 minutes
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(new Error('Login timeout - no callback received'));
|
|
162
|
+
}, 5 * 60 * 1000);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export async function refreshToken(alias) {
|
|
166
|
+
const store = loadStore();
|
|
167
|
+
const account = store.accounts[alias];
|
|
168
|
+
if (!account?.refreshToken) {
|
|
169
|
+
console.error(`[multi-auth] No refresh token for ${alias}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
176
|
+
body: new URLSearchParams({
|
|
177
|
+
grant_type: 'refresh_token',
|
|
178
|
+
client_id: CLIENT_ID,
|
|
179
|
+
refresh_token: account.refreshToken
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
if (!tokenRes.ok) {
|
|
183
|
+
console.error(`[multi-auth] Refresh failed for ${alias}: ${tokenRes.status}`);
|
|
184
|
+
// If the refresh token is invalid/expired, mark this account invalid so
|
|
185
|
+
// rotation can keep working without repeatedly selecting a broken account.
|
|
186
|
+
if (tokenRes.status === 401 || tokenRes.status === 403) {
|
|
187
|
+
try {
|
|
188
|
+
updateAccount(alias, {
|
|
189
|
+
authInvalid: true,
|
|
190
|
+
authInvalidatedAt: Date.now()
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// ignore
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const tokens = (await tokenRes.json());
|
|
200
|
+
const accessClaims = decodeJwtPayload(tokens.access_token);
|
|
201
|
+
const idClaims = tokens.id_token ? decodeJwtPayload(tokens.id_token) : null;
|
|
202
|
+
const expiresAt = getExpiryFromClaims(accessClaims) || getExpiryFromClaims(idClaims) || Date.now() + tokens.expires_in * 1000;
|
|
203
|
+
const updates = {
|
|
204
|
+
accessToken: tokens.access_token,
|
|
205
|
+
refreshToken: tokens.refresh_token || account.refreshToken,
|
|
206
|
+
expiresAt,
|
|
207
|
+
lastRefresh: new Date().toISOString(),
|
|
208
|
+
idToken: tokens.id_token || account.idToken,
|
|
209
|
+
accountId: getAccountIdFromClaims(idClaims) ||
|
|
210
|
+
getAccountIdFromClaims(accessClaims) ||
|
|
211
|
+
account.accountId
|
|
212
|
+
};
|
|
213
|
+
const updatedStore = updateAccount(alias, updates);
|
|
214
|
+
clearAuthInvalid(alias);
|
|
215
|
+
return updatedStore.accounts[alias];
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
console.error(`[multi-auth] Refresh error for ${alias}:`, err);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export async function ensureValidToken(alias) {
|
|
223
|
+
const store = loadStore();
|
|
224
|
+
const account = store.accounts[alias];
|
|
225
|
+
if (!account)
|
|
226
|
+
return null;
|
|
227
|
+
// Refresh if expiring within 5 minutes
|
|
228
|
+
const bufferMs = 5 * 60 * 1000;
|
|
229
|
+
if (account.expiresAt < Date.now() + bufferMs) {
|
|
230
|
+
console.log(`[multi-auth] Refreshing token for ${alias}`);
|
|
231
|
+
const refreshed = await refreshToken(alias);
|
|
232
|
+
return refreshed?.accessToken || null;
|
|
233
|
+
}
|
|
234
|
+
return account.accessToken;
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=auth.js.map
|