aisubs 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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +629 -0
  3. package/dashboard/public/aisubs-mark.svg +11 -0
  4. package/dist/account-key.d.ts +2 -0
  5. package/dist/account-key.js +11 -0
  6. package/dist/auth.d.ts +76 -0
  7. package/dist/auth.js +477 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +97 -0
  10. package/dist/dashboard/aisubs-mark.svg +11 -0
  11. package/dist/dashboard/assets/index-BMoILzPw.js +64 -0
  12. package/dist/dashboard/assets/index-DHqDdNVe.css +2 -0
  13. package/dist/dashboard/index.html +15 -0
  14. package/dist/dashboard/logos/anthropic.svg +3 -0
  15. package/dist/dashboard/logos/github-copilot.svg +3 -0
  16. package/dist/dashboard/logos/google.svg +3 -0
  17. package/dist/dashboard/logos/openai.svg +3 -0
  18. package/dist/dashboard/logos/opencode-dark.svg +18 -0
  19. package/dist/dashboard/logos/opencode-light.svg +18 -0
  20. package/dist/dashboard/logos/xai.svg +3 -0
  21. package/dist/dashboard.d.ts +18 -0
  22. package/dist/dashboard.js +139 -0
  23. package/dist/http.d.ts +22 -0
  24. package/dist/http.js +263 -0
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.js +8 -0
  27. package/dist/providers/chatgpt.d.ts +7 -0
  28. package/dist/providers/chatgpt.js +402 -0
  29. package/dist/providers/claude.d.ts +7 -0
  30. package/dist/providers/claude.js +289 -0
  31. package/dist/providers/copilot.d.ts +6 -0
  32. package/dist/providers/copilot.js +466 -0
  33. package/dist/providers/grok.d.ts +7 -0
  34. package/dist/providers/grok.js +215 -0
  35. package/dist/providers/opencode.d.ts +4 -0
  36. package/dist/providers/opencode.js +147 -0
  37. package/dist/store.d.ts +18 -0
  38. package/dist/store.js +121 -0
  39. package/dist/types.d.ts +186 -0
  40. package/dist/types.js +1 -0
  41. package/dist/usage.d.ts +5 -0
  42. package/dist/usage.js +418 -0
  43. package/dist/utils.d.ts +11 -0
  44. package/dist/utils.js +68 -0
  45. package/examples/direct.mjs +38 -0
  46. package/examples/server.mjs +20 -0
  47. package/package.json +122 -0
  48. package/public/aisubs-chatgpt-account.png +0 -0
  49. package/public/aisubs-copilot-account.png +0 -0
  50. package/public/aisubs-dashboard.png +0 -0
  51. package/public/aisubs-grok-account.png +0 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AISubs 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,629 @@
1
+ <table>
2
+ <tr>
3
+ <td><img src="./dashboard/public/aisubs-mark.svg" alt="AISubs icon" width="72"></td>
4
+ <td>
5
+ <h1>AISubs</h1>
6
+ <strong>Connect your AI subscriptions. Use them anywhere.</strong>
7
+ </td>
8
+ </tr>
9
+ </table>
10
+
11
+ Connect once, then use your subscriptions through an SDK in your app, a local
12
+ API, cURL, or any compatible tool.
13
+
14
+ <p align="center">
15
+ <img src="./public/aisubs-dashboard.png" alt="AISubs dashboard showing connected accounts" width="100%" />
16
+ </p>
17
+
18
+ <p align="center">
19
+ <img src="./public/aisubs-chatgpt-account.png" alt="AISubs ChatGPT account details showing plan, usage, and integration examples" width="33%" />
20
+ <img src="./public/aisubs-grok-account.png" alt="AISubs Grok account details showing plan, usage, and integration examples" width="33%" />
21
+ <img src="./public/aisubs-copilot-account.png" alt="AISubs GitHub Copilot account details showing credits and integration examples" width="33%" />
22
+ </p>
23
+
24
+ > AISubs keeps credentials on your computer. It collects no telemetry, analytics, request logs, or activity history.
25
+
26
+ ## Quick start
27
+
28
+ ```bash
29
+ nubx aisubs dashboard # Nub (recommended)
30
+ npx aisubs dashboard # npm
31
+ pnpm exec aisubs dashboard # pnpm
32
+ bunx aisubs dashboard # Bun
33
+ ```
34
+
35
+ Click **Add account**, choose a provider, complete sign-in, and give the
36
+ account a local name such as `personal` or `work`.
37
+
38
+ ## The idea
39
+
40
+ 1. Connect a provider account and give it a local name, such as `personal`.
41
+ 2. Ask AISubs which models and request format that account supports.
42
+ 3. Send the provider-native request through that account.
43
+
44
+ One provider can have many accounts:
45
+
46
+ ```text
47
+ ChatGPT / personal
48
+ ChatGPT / work
49
+ Claude / team
50
+ ```
51
+
52
+ AISubs never silently switches accounts. Your application chooses the account
53
+ for each request.
54
+
55
+ ## Providers
56
+
57
+ | Provider | ID | Sign-in | Request format |
58
+ | -------------- | -------------- | ---------------------- | --------------------------- |
59
+ | ChatGPT | `chatgpt` | Browser or device code | Responses |
60
+ | Claude | `claude` | Browser | Anthropic Messages |
61
+ | GitHub Copilot | `copilot` | Device code | Read from the model catalog |
62
+ | Grok | `grok` | Device code | Read from the model catalog |
63
+ | OpenCode Go | `opencode-go` | API key | Read from the model catalog |
64
+ | OpenCode Zen | `opencode-zen` | API key | Read from the model catalog |
65
+
66
+ Provider model lists and protocols can change. Discover models at runtime and
67
+ pin the AISubs version your application has tested.
68
+
69
+ ## Fastest start: direct Node.js
70
+
71
+ This is the simplest integration. It needs no local server and no AISubs API
72
+ key.
73
+
74
+ ### 1. Create AISubs once
75
+
76
+ ```js
77
+ // subscriptions.js
78
+ import { chatGptProvider, createSubscriptionAuth } from "aisubs";
79
+
80
+ export const subscriptions = createSubscriptionAuth({
81
+ providers: [chatGptProvider()],
82
+ });
83
+ ```
84
+
85
+ By default, credentials are stored at `~/.aisubs/credentials.json`.
86
+
87
+ Keep this object in trusted backend code. Do not send it to a browser.
88
+
89
+ ### 2. Connect an account
90
+
91
+ ```js
92
+ import { subscriptions } from "./subscriptions.js";
93
+
94
+ const account = subscriptions.account("chatgpt", "personal");
95
+
96
+ if (!(await account.status()).authenticated) {
97
+ const login = await account.signIn();
98
+
99
+ if (login.prompt.mode === "browser") {
100
+ console.log("Open:", login.prompt.authorizationUri);
101
+ }
102
+ if (login.prompt.mode === "device") {
103
+ console.log("Open:", login.prompt.verificationUri);
104
+ console.log("Code:", login.prompt.userCode);
105
+ }
106
+
107
+ await login.wait();
108
+ }
109
+ ```
110
+
111
+ ### 3. Discover a model and send a request
112
+
113
+ This example selects the first available ChatGPT model, so it does not depend
114
+ on a model ID that may change:
115
+
116
+ ```js
117
+ const catalog = await account.getModels();
118
+ if (!catalog) throw new Error("This provider does not expose models");
119
+ console.log(
120
+ "Available model IDs:",
121
+ catalog.models.map((item) => item.id),
122
+ );
123
+
124
+ const modelId = catalog.models.find((item) => item.selectable !== false)?.id;
125
+ if (!modelId) throw new Error("No ChatGPT model is available");
126
+ console.log("Using model:", modelId);
127
+
128
+ const response = await account.proxy("responses", {
129
+ method: "POST",
130
+ headers: { "content-type": "application/json" },
131
+ body: JSON.stringify({
132
+ model: modelId,
133
+ store: false,
134
+ stream: true,
135
+ input: "Hello from AISubs",
136
+ }),
137
+ });
138
+
139
+ if (!response.ok) throw new Error(await response.text());
140
+ if (response.body) {
141
+ for await (const chunk of response.body) process.stdout.write(Buffer.from(chunk));
142
+ }
143
+ ```
144
+
145
+ Here, `modelId` is the exact ID returned by `account.getModels()`. Model IDs
146
+ belong to the connected provider account and can change, so list them first
147
+ instead of copying a fixed ID from documentation.
148
+
149
+ `store: false` is a request option for the Responses API; it is unrelated to
150
+ AISubs credential storage and keeps response storage disabled. `stream: true`
151
+ asks for incremental output, which is why the example reads `response.body` in
152
+ chunks. Keep both options for ChatGPT subscription Responses requests.
153
+
154
+ AISubs refreshes expired credentials automatically and retries a provider `401`
155
+ once.
156
+
157
+ <details>
158
+ <summary><strong>Use another provider</strong></summary>
159
+
160
+ Add its provider factory when creating AISubs:
161
+
162
+ ```js
163
+ import {
164
+ chatGptProvider,
165
+ claudeProvider,
166
+ copilotProvider,
167
+ createSubscriptionAuth,
168
+ grokProvider,
169
+ openCodeGoProvider,
170
+ openCodeZenProvider,
171
+ } from "aisubs";
172
+
173
+ const subscriptions = createSubscriptionAuth({
174
+ providers: [
175
+ chatGptProvider(),
176
+ claudeProvider(),
177
+ copilotProvider(),
178
+ grokProvider(),
179
+ openCodeGoProvider(),
180
+ openCodeZenProvider(),
181
+ ],
182
+ });
183
+ ```
184
+
185
+ To use a different file, pass a custom store:
186
+
187
+ ```js
188
+ import { chatGptProvider, createSubscriptionAuth, FileCredentialStore } from "aisubs";
189
+
190
+ const subscriptions = createSubscriptionAuth({
191
+ store: new FileCredentialStore("./data/aisubs-credentials.json"),
192
+ providers: [chatGptProvider()],
193
+ });
194
+ ```
195
+
196
+ Then select the account by provider ID:
197
+
198
+ ```js
199
+ const claude = subscriptions.account("claude", "team");
200
+ const copilot = subscriptions.account("copilot", "github");
201
+ const grok = subscriptions.account("grok", "personal");
202
+ const go = subscriptions.account("opencode-go", "team");
203
+ const zen = subscriptions.account("opencode-zen", "lab");
204
+ ```
205
+
206
+ OpenCode uses an API key:
207
+
208
+ ```js
209
+ const login = await go.signIn({ apiKey: process.env.OPENCODE_API_KEY });
210
+ await login.wait();
211
+ ```
212
+
213
+ For ChatGPT on a headless machine, use `signIn({ mode: "device" })`. Copilot
214
+ also accepts `enterpriseDomain` for a supported GitHub Enterprise Cloud domain.
215
+
216
+ Provider request paths are:
217
+
218
+ | Model catalog endpoint | `account.proxy()` path |
219
+ | ---------------------- | --------------------------------- |
220
+ | `responses` | `responses` |
221
+ | `chat/completions` | `chat/completions` |
222
+ | `messages` | `messages` |
223
+ | `models/MODEL_ID` | `models/MODEL_ID:generateContent` |
224
+
225
+ Use `account.getModels()` first for Copilot, Grok, and OpenCode because one
226
+ provider can expose more than one request format.
227
+
228
+ </details>
229
+
230
+ <details>
231
+ <summary><strong>Read account, usage, and model information</strong></summary>
232
+
233
+ ```js
234
+ const details = await account.details();
235
+
236
+ console.log(details.session); // connection state and safe account identity
237
+ console.log(details.credential); // expiry and refresh state, never token values
238
+ console.log(details.usage); // limits and reset information, or null
239
+ console.log(details.models); // available models, or null
240
+ ```
241
+
242
+ Useful methods:
243
+
244
+ | Method | Purpose |
245
+ | ---------------------------- | ------------------------------------------------------ |
246
+ | `account.status()` | Check whether the account is connected |
247
+ | `account.signIn(options?)` | Start a browser, device-code, or API-key login |
248
+ | `account.signOut()` | Remove this account's locally stored credentials |
249
+ | `account.getModels()` | Get the provider's current model catalog |
250
+ | `account.getUsage()` | Get current plan usage, if supported |
251
+ | `account.details()` | Get safe session, credential, usage, and model data |
252
+ | `account.fetch(url, init?)` | Make an authorized request to an allowed provider URL |
253
+ | `account.proxy(path, init?)` | Make a provider-native request without handling tokens |
254
+
255
+ `details()`, `getUsage()`, and `getModels()` never return access or refresh
256
+ tokens. `getAccessToken()` exists for advanced backend integrations; keep its
257
+ result secret and prefer `fetch()` or `proxy()` when possible.
258
+
259
+ </details>
260
+
261
+ <details>
262
+ <summary><strong>Use multiple accounts</strong></summary>
263
+
264
+ ```js
265
+ const personal = subscriptions.account("chatgpt", "personal");
266
+ const work = subscriptions.account("chatgpt", "work");
267
+
268
+ const selected = user.isWorkAccount ? work : personal;
269
+ const response = await selected.proxy("responses", requestOptions);
270
+ ```
271
+
272
+ Account names are 1–128 characters and cannot contain control characters.
273
+ Each account has separate credentials, refresh state, usage, and model data.
274
+
275
+ </details>
276
+
277
+ ## Dashboard
278
+
279
+ ### Requirements and installation
280
+
281
+ - [Node.js 24 or newer](https://nodejs.org/en/download/).
282
+ - A terminal and a browser for provider sign-in.
283
+ - [Nub 0.6 or newer](https://nubjs.com/docs/install) is recommended. It is not
284
+ required; [pnpm](https://pnpm.io/installation),
285
+ [npm](https://docs.npmjs.com/cli/install/), and
286
+ [Bun](https://bun.sh/docs/installation) also work.
287
+ - An API key for OpenCode Go or OpenCode Zen; the other providers use browser
288
+ or device-code sign-in.
289
+
290
+ Install AISubs in your project:
291
+
292
+ ```bash
293
+ nub install aisubs # Nub (recommended)
294
+ npm install aisubs # npm
295
+ pnpm add aisubs # pnpm
296
+ bun add aisubs # Bun
297
+ ```
298
+
299
+ Use the dashboard when you want to connect accounts without writing login UI:
300
+
301
+ ```bash
302
+ nubx aisubs dashboard # Nub (recommended)
303
+ npx aisubs dashboard # npm
304
+ pnpm exec aisubs dashboard # pnpm
305
+ bunx aisubs dashboard # Bun
306
+ ```
307
+
308
+ Then click **Add account**, choose a provider, finish sign-in, and choose a
309
+ local account name. The dashboard shows safe account details, usage, models,
310
+ and copy-ready integration examples.
311
+
312
+ By default, AISubs uses Node.js 24 or newer, listens on
313
+ `127.0.0.1:4319`, and stores credentials at `~/.aisubs/credentials.json`.
314
+
315
+ To use another directory, choose an available port, or prevent the browser
316
+ from opening:
317
+
318
+ ```bash
319
+ nubx aisubs dashboard \
320
+ --data-dir ./data/aisubs \
321
+ --port 0 \
322
+ --no-open
323
+ ```
324
+
325
+ Use the equivalent `npx`, `pnpm exec`, or `bunx` command if you use npm, pnpm,
326
+ or Bun.
327
+
328
+ The dashboard prints the secure link when `--no-open` is used. Credentials stay
329
+ on your computer; do not commit the data directory or expose the dashboard to
330
+ the network.
331
+
332
+ Use `AISUBS_DATA_DIR` or `--data-dir` to choose another data directory. Use
333
+ `--port 0` for an available port and `--no-open` to print the secure link
334
+ without opening a browser.
335
+
336
+ ## Local HTTP bridge
337
+
338
+ Use the bridge for an existing SDK, Python, cURL, or another program that
339
+ cannot import AISubs:
340
+
341
+ ```bash
342
+ nubx aisubs dashboard # Nub (recommended)
343
+ npx aisubs dashboard # npm
344
+ pnpm exec aisubs dashboard # pnpm
345
+ bunx aisubs dashboard # Bun
346
+ export AISUBS_API_KEY="the-control-key-printed-by-aisubs"
347
+ ```
348
+
349
+ The base URL chooses the provider and account. Append the provider's request
350
+ path shown by `getModels()` or the dashboard:
351
+
352
+ ```text
353
+ http://127.0.0.1:4319/aisubs/chatgpt/personal/responses
354
+ http://127.0.0.1:4319/aisubs/claude/team/messages
355
+ http://127.0.0.1:4319/aisubs/grok/personal/chat/completions
356
+ ```
357
+
358
+ The account name is URL-decoded by AISubs, so URL-encode names containing
359
+ spaces or other URL characters. AISubs removes the control key before sending
360
+ the request to a provider.
361
+
362
+ <details>
363
+ <summary><strong>Vercel AI SDK</strong></summary>
364
+
365
+ For a `responses` model:
366
+
367
+ ```bash
368
+ nub install ai @ai-sdk/openai
369
+ ```
370
+
371
+ ```js
372
+ import { createOpenAI } from "@ai-sdk/openai";
373
+ import { streamText } from "ai";
374
+
375
+ const provider = createOpenAI({
376
+ baseURL: "http://127.0.0.1:4319/aisubs/chatgpt/personal",
377
+ apiKey: process.env.AISUBS_API_KEY,
378
+ });
379
+
380
+ const result = streamText({
381
+ model: provider.responses("MODEL_ID"),
382
+ prompt: "Hello",
383
+ providerOptions: { openai: { store: false } },
384
+ });
385
+
386
+ for await (const text of result.textStream) process.stdout.write(text);
387
+ ```
388
+
389
+ For `chat/completions`, use `@ai-sdk/openai-compatible`. For `messages`, use
390
+ `@ai-sdk/anthropic`. For OpenCode Zen Gemini, use the model-specific
391
+ `models/MODEL_ID:generateContent` URL shown in the dashboard.
392
+
393
+ </details>
394
+
395
+ <details>
396
+ <summary><strong>TanStack AI</strong></summary>
397
+
398
+ TanStack AI can use AISubs through its OpenAI-compatible adapter:
399
+
400
+ ```bash
401
+ nub install @tanstack/ai @tanstack/ai-openai # Nub (recommended)
402
+ npm install @tanstack/ai @tanstack/ai-openai # npm
403
+ pnpm add @tanstack/ai @tanstack/ai-openai # pnpm
404
+ bun add @tanstack/ai @tanstack/ai-openai # Bun
405
+ ```
406
+
407
+ ```ts
408
+ import { chat } from "@tanstack/ai";
409
+ import { openaiCompatibleText } from "@tanstack/ai-openai/compatible";
410
+
411
+ const stream = chat({
412
+ adapter: openaiCompatibleText("MODEL_ID", {
413
+ baseURL: "http://127.0.0.1:4319/aisubs/grok/personal",
414
+ apiKey: process.env.AISUBS_API_KEY!,
415
+ }),
416
+ messages: [{ role: "user", content: "Hello" }],
417
+ });
418
+
419
+ for await (const chunk of stream) {
420
+ if (chunk.type === "TEXT_MESSAGE_CONTENT") process.stdout.write(chunk.delta);
421
+ }
422
+ ```
423
+
424
+ Use the equivalent `npm install`, `pnpm add`, or `bun add` command if you use
425
+ another package manager. Replace the provider, account, and model with values
426
+ from your dashboard.
427
+
428
+ </details>
429
+
430
+ <details>
431
+ <summary><strong>OpenAI, Anthropic, Python, or cURL</strong></summary>
432
+
433
+ OpenAI Responses:
434
+
435
+ ```bash
436
+ nub install openai
437
+ ```
438
+
439
+ ```js
440
+ import OpenAI from "openai";
441
+
442
+ const client = new OpenAI({
443
+ baseURL: "http://127.0.0.1:4319/aisubs/chatgpt/personal",
444
+ apiKey: process.env.AISUBS_API_KEY,
445
+ });
446
+
447
+ const stream = await client.responses.create({
448
+ model: "MODEL_ID",
449
+ store: false,
450
+ stream: true,
451
+ input: "Hello",
452
+ });
453
+
454
+ for await (const event of stream) console.log(event);
455
+ ```
456
+
457
+ Anthropic Messages uses `@anthropic-ai/sdk` with this base URL:
458
+
459
+ ```text
460
+ http://127.0.0.1:4319/aisubs/claude/team
461
+ ```
462
+
463
+ Chat Completions with LiteLLM:
464
+
465
+ ```bash
466
+ pip install litellm
467
+ ```
468
+
469
+ ```python
470
+ import os
471
+ from litellm import completion
472
+
473
+ response = completion(
474
+ model="openai/MODEL_ID",
475
+ api_base="http://127.0.0.1:4319/aisubs/grok/personal",
476
+ api_key=os.environ["AISUBS_API_KEY"],
477
+ messages=[{"role": "user", "content": "Hello"}],
478
+ stream=True,
479
+ )
480
+
481
+ for event in response:
482
+ print(event)
483
+ ```
484
+
485
+ cURL:
486
+
487
+ ```bash
488
+ curl "http://127.0.0.1:4319/aisubs/chatgpt/personal/responses" \
489
+ -H "Authorization: Bearer $AISUBS_API_KEY" \
490
+ -H "Content-Type: application/json" \
491
+ -d '{"model":"MODEL_ID","store":false,"stream":true,"input":"Hello"}'
492
+ ```
493
+
494
+ For Messages, use `x-api-key: $AISUBS_API_KEY` and
495
+ `anthropic-version: 2023-06-01`.
496
+
497
+ </details>
498
+
499
+ <details>
500
+ <summary><strong>Run a server from Node.js</strong></summary>
501
+
502
+ HTTP bridge without the dashboard:
503
+
504
+ ```js
505
+ import { randomBytes } from "node:crypto";
506
+ import { chatGptProvider, createSubscriptionAuth, FileCredentialStore } from "aisubs";
507
+ import { createSubscriptionAuthServer } from "aisubs/http";
508
+
509
+ const auth = createSubscriptionAuth({
510
+ store: new FileCredentialStore("./data/aisubs-credentials.json"),
511
+ providers: [chatGptProvider()],
512
+ });
513
+
514
+ const server = await createSubscriptionAuthServer({
515
+ auth,
516
+ apiKey: randomBytes(24).toString("hex"),
517
+ port: 4319,
518
+ });
519
+
520
+ console.log(server.url);
521
+ // await server.close();
522
+ ```
523
+
524
+ Dashboard inside a Node.js application:
525
+
526
+ ```js
527
+ import { createSubscriptionAuthDashboardServer } from "aisubs/dashboard";
528
+
529
+ const dashboard = await createSubscriptionAuthDashboardServer({ auth });
530
+ console.log(dashboard.bootstrapUrl);
531
+ // await dashboard.close();
532
+ ```
533
+
534
+ Both servers bind only to localhost. The programmatic HTTP server requires its
535
+ API key; the dashboard also provides a one-time browser link.
536
+
537
+ </details>
538
+
539
+ <details>
540
+ <summary><strong>Local HTTP API</strong></summary>
541
+
542
+ ```text
543
+ GET /health
544
+ GET /v1/providers
545
+ GET /v1/auth
546
+ GET /v1/auth/:provider
547
+ GET /v1/auth/:provider/accounts
548
+ POST /v1/auth/:provider/login
549
+ GET /v1/logins/:loginId
550
+ DELETE /v1/logins/:loginId
551
+ GET /v1/auth/:provider/details?account=work
552
+ DELETE /v1/auth/:provider?account=work
553
+ POST /v1/fetch/:provider
554
+ GET /v1/usage/:provider?account=work
555
+ GET /v1/models/:provider?account=work
556
+ * /aisubs/:provider/:account/*
557
+ ```
558
+
559
+ The dashboard's `/bootstrap` link is one-time. Other routes require the
560
+ control API key or dashboard session cookie. Login responses return an attempt
561
+ ID; poll `/v1/logins/:loginId` until it is complete, failed, or cancelled.
562
+
563
+ </details>
564
+
565
+ <details>
566
+ <summary><strong>Storage and security</strong></summary>
567
+
568
+ - Default credentials: `~/.aisubs/credentials.json`.
569
+ - Override the directory with `AISUBS_DATA_DIR` or `--data-dir`.
570
+ - `FileCredentialStore` creates private directories/files and uses file locks.
571
+ - `MemoryCredentialStore` is available for tests and temporary processes.
572
+ - Usage is cached for 15 seconds; model catalogs are cached for five minutes.
573
+ - Sign-in, refresh, and sign-out clear the affected metadata cache.
574
+ - Provider credentials are added only after host allowlist validation.
575
+ - Local auth and control-key headers are removed before forwarding.
576
+ - Account APIs return safe summaries, never token values.
577
+ - Never expose provider credentials or `AISUBS_API_KEY` in browser code.
578
+
579
+ </details>
580
+
581
+ <details>
582
+ <summary><strong>Local development</strong></summary>
583
+
584
+ From the package directory, run:
585
+
586
+ ```bash
587
+ nub run dev
588
+ ```
589
+
590
+ Equivalent commands are `pnpm dev`, `npm run dev`, and `bun run dev`. Nub is
591
+ recommended, but it is not required. The command builds the package once,
592
+ watches backend and dashboard changes, and opens the local dashboard. Pass
593
+ `-- --no-open` to keep the browser closed.
594
+
595
+ </details>
596
+
597
+ <details>
598
+ <summary><strong>Maintainer pre-publish check</strong></summary>
599
+
600
+ ```bash
601
+ nub run check
602
+ nub pack --dry-run
603
+ ```
604
+
605
+ With another package manager, use `pnpm check` / `pnpm pack`,
606
+ `npm run check` / `npm pack --dry-run`, or `bun run check` / `bun pm pack`.
607
+
608
+ Confirm that the package contains `dist`, `examples`, `public`, `README.md`,
609
+ `LICENSE`, and the README logo asset. Test at least one real account for every
610
+ provider your release claims to support.
611
+
612
+ Runnable examples:
613
+
614
+ - [`examples/direct.mjs`](./examples/direct.mjs)
615
+ - [`examples/server.mjs`](./examples/server.mjs)
616
+
617
+ </details>
618
+
619
+ ## Contributing and bug reports
620
+
621
+ Please read [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening an issue or
622
+ pull request. To report a reproducible bug, use the
623
+ [Bug report form](./.github/ISSUE_TEMPLATE/bug_report.yml) and include the
624
+ version, environment, steps to reproduce, expected and actual behavior, and
625
+ sanitized error output where relevant.
626
+
627
+ ## License
628
+
629
+ AISubs is licensed under the [MIT License](./LICENSE).
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56">
2
+ <rect width="56" height="56" rx="14" fill="#e5e5e5"/>
3
+ <path
4
+ fill="#0f0f0f"
5
+ fill-rule="evenodd"
6
+ d="M11.25 25.12C11.34 24.55 11.74 22.98 12 22.38C12.26 21.77 12.21 21.23 13.38 20.12C14.54 19.02 20.49 14.16 21.75 13.12C23.01 12.09 23.27 11.86 23.88 11.5C24.48 11.14 26.18 10.34 26.75 10.12C27.32 9.91 28.1 9.79 28.62 9.75C29.15 9.71 30.61 9.71 31.12 9.75C31.64 9.79 32.41 9.96 32.88 10.12C33.34 10.29 34.45 10.72 35 11.12C35.55 11.53 37.01 12.81 37.5 13.5C37.99 14.19 38.9 16.16 39.12 16.88C39.35 17.59 39.08 19.04 39.38 19.5C39.67 19.96 41.15 20.38 41.62 20.75C42.1 21.12 43.05 22.19 43.38 22.62C43.7 23.06 44.16 23.8 44.38 24.38C44.59 24.95 45.03 26.77 45.12 27.38C45.22 27.98 45.2 28.84 45.12 29.38C45.05 29.91 44.78 31.23 44.5 31.88C44.22 32.52 44.1 33.46 42.75 34.75C41.4 36.04 34.78 41.47 33.25 42.62C31.72 43.78 30.59 44.12 30 44.38C29.41 44.63 28.93 44.7 28.38 44.75C27.82 44.8 26.18 44.91 25.38 44.75C24.57 44.59 22.41 43.83 21.62 43.38C20.84 42.92 19.4 41.65 18.88 41C18.35 40.35 17.5 38.77 17.25 38C17 37.23 17.04 35.15 16.75 34.62C16.46 34.1 15.29 33.94 14.88 33.62C14.46 33.31 13.6 32.44 13.25 32C12.9 31.57 12.24 30.59 12 30C11.76 29.41 11.34 27.71 11.25 27.12C11.16 26.54 11.16 25.7 11.25 25.12Z
7
+ M14.75 25.75C14.72 26.45 15.12 28.27 15.38 28.88C15.63 29.48 16.61 30.5 16.88 30.75C17.14 31 17.48 31.14 17.62 31C17.77 30.86 17.95 29.95 18.12 29.62C18.3 29.3 18.66 28.71 19.12 28.25C19.59 27.79 20.7 26.75 22 25.75C23.3 24.75 28.65 20.73 30 19.88C31.35 19.02 32.56 18.79 33.25 18.62C33.94 18.46 35.48 18.68 35.75 18.5C36.02 18.32 35.74 17.59 35.5 17.12C35.26 16.66 34.2 15.06 33.75 14.62C33.3 14.19 32.34 13.66 31.75 13.5C31.16 13.34 29.49 13.21 28.88 13.25C28.26 13.29 28.07 12.88 26.62 13.88C25.18 14.87 18.2 20.41 16.88 21.5C15.55 22.59 15.88 22.49 15.62 23C15.37 23.51 14.78 25.05 14.75 25.75Z
8
+ M20.75 32C20.68 32.16 20.5 32.23 20.75 32.25C21 32.27 22.38 32.24 22.88 32.12C23.37 32.01 23.63 32.13 24.88 31.25C26.12 30.36 32.06 25.73 33.25 24.75C34.44 23.77 34.51 23.45 34.75 23.12C34.99 22.8 35.35 22.14 35.25 22C35.15 21.86 34.31 21.89 33.88 22C33.44 22.11 32.98 21.96 31.62 22.88C30.27 23.79 23.86 28.66 22.62 29.62C21.39 30.59 21.6 30.59 21.38 30.88C21.15 31.16 20.82 31.84 20.75 32Z
9
+ M20.5 35.75C20.16 36.11 21.05 37.84 21.38 38.38C21.7 38.91 22.71 39.91 23.25 40.25C23.79 40.59 25.32 41.13 25.88 41.25C26.43 41.37 27.45 41.3 27.88 41.25C28.3 41.2 29.05 40.99 29.38 40.88C29.7 40.76 29.41 41.16 30.62 40.25C31.84 39.34 38.34 34.2 39.5 33.25C40.66 32.3 40.04 32.69 40.25 32.38C40.46 32.06 41.09 31.02 41.25 30.62C41.41 30.23 41.62 29.68 41.62 29.12C41.62 28.57 41.43 26.6 41.25 26C41.07 25.4 40.44 24.48 40.12 24.12C39.81 23.77 38.9 22.93 38.62 23C38.35 23.07 38.2 24.25 37.88 24.75C37.55 25.25 37.18 26.02 35.88 27.12C34.57 28.23 28.39 33.01 27 34C25.61 34.99 25.03 35.16 24.25 35.38C23.47 35.59 20.84 35.39 20.5 35.75Z"
10
+ />
11
+ </svg>
@@ -0,0 +1,2 @@
1
+ import type { ProviderId, Session } from "./types.js";
2
+ export declare function nextAccountKey(sessions: readonly Session[], provider: ProviderId): string;
@@ -0,0 +1,11 @@
1
+ export function nextAccountKey(sessions, provider) {
2
+ const used = new Set(sessions
3
+ .filter((session) => session.provider === provider && session.authenticated)
4
+ .map((session) => session.accountKey));
5
+ if (!used.has("default"))
6
+ return "default";
7
+ let index = 2;
8
+ while (used.has(`account-${index}`))
9
+ index += 1;
10
+ return `account-${index}`;
11
+ }