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/dist/auth.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { CredentialStore, CredentialSummary, LoginAttempt, LoginState, ProviderAdapter, ProviderId, ProviderModels, ProviderSummary, ProviderUsage, Session, SubscriptionAccountDetails } from "./types.js";
2
+ export interface SubscriptionAuthOptions {
3
+ refreshTimeoutMs?: number;
4
+ /** Successful usage snapshots stay in memory briefly; credentials never do. */
5
+ usageCacheTtlMs?: number;
6
+ /** Successful model catalogs stay in memory briefly; credentials never do. */
7
+ modelsCacheTtlMs?: number;
8
+ }
9
+ export declare const DEFAULT_ACCOUNT = "default";
10
+ export interface SubscriptionAccount {
11
+ readonly provider: ProviderId;
12
+ readonly accountKey: string;
13
+ signIn(options?: Record<string, unknown>): Promise<LoginAttempt>;
14
+ status(options?: {
15
+ validate?: boolean;
16
+ }): Promise<Session>;
17
+ signOut(): Promise<void>;
18
+ getAccessToken(): Promise<string>;
19
+ fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
20
+ proxy(path: string, init?: RequestInit): Promise<Response>;
21
+ getUsage(signal?: AbortSignal): Promise<ProviderUsage | null>;
22
+ getModels(signal?: AbortSignal): Promise<ProviderModels | null>;
23
+ credentialSummary(): Promise<CredentialSummary>;
24
+ details(signal?: AbortSignal): Promise<SubscriptionAccountDetails>;
25
+ }
26
+ export declare class SubscriptionAuth {
27
+ readonly store: CredentialStore;
28
+ private readonly adapters;
29
+ private readonly attempts;
30
+ private readonly generations;
31
+ private readonly refreshes;
32
+ private readonly usageCache;
33
+ private readonly modelsCache;
34
+ private readonly usageInflight;
35
+ private readonly modelsInflight;
36
+ private readonly metadataGenerations;
37
+ private readonly refreshTimeoutMs;
38
+ private readonly usageCacheTtlMs;
39
+ private readonly modelsCacheTtlMs;
40
+ constructor(store: CredentialStore, providers: readonly ProviderAdapter[], options?: SubscriptionAuthOptions);
41
+ listProviders(): ProviderSummary[];
42
+ private adapter;
43
+ private generation;
44
+ private advance;
45
+ private clearMetadata;
46
+ private cachedMetadata;
47
+ signIn(provider: ProviderId, options?: Record<string, unknown>): Promise<LoginAttempt>;
48
+ private expireLoginAttempt;
49
+ getLoginAttempt(id: string): {
50
+ provider: ProviderId;
51
+ accountKey: string;
52
+ state: LoginState;
53
+ error: string | null;
54
+ } | null;
55
+ cancelLoginAttempt(id: string): boolean;
56
+ status(provider: ProviderId, options?: {
57
+ validate?: boolean;
58
+ account?: string;
59
+ }): Promise<Session>;
60
+ statuses(): Promise<Session[]>;
61
+ listAccounts(provider: ProviderId): Promise<Session[]>;
62
+ signOut(provider: ProviderId, account?: string): Promise<void>;
63
+ private credential;
64
+ getAccessToken(provider: ProviderId, account?: string): Promise<string>;
65
+ credentialSummary(provider: ProviderId, account?: string): Promise<CredentialSummary>;
66
+ details(provider: ProviderId, account?: string, signal?: AbortSignal): Promise<SubscriptionAccountDetails>;
67
+ fetch(provider: ProviderId, input: string | URL | Request, init?: RequestInit, account?: string): Promise<Response>;
68
+ proxy(provider: ProviderId, account: string, path: string, init?: RequestInit): Promise<Response>;
69
+ getUsage(provider: ProviderId, account?: string, callerSignal?: AbortSignal): Promise<ProviderUsage | null>;
70
+ getModels(provider: ProviderId, account?: string, callerSignal?: AbortSignal): Promise<ProviderModels | null>;
71
+ account(provider: ProviderId, account: string): SubscriptionAccount;
72
+ }
73
+ export declare function createSubscriptionAuth(options: {
74
+ store?: CredentialStore;
75
+ providers: readonly ProviderAdapter[];
76
+ } & SubscriptionAuthOptions): SubscriptionAuth;
package/dist/auth.js ADDED
@@ -0,0 +1,477 @@
1
+ import { defaultAiSubsDataDir, FileCredentialStore } from "./store.js";
2
+ import { errorMessage } from "./utils.js";
3
+ import { join } from "node:path";
4
+ export const DEFAULT_ACCOUNT = "default";
5
+ const ACCOUNT_STORAGE_PREFIX = "$subscription-account$";
6
+ const LOGIN_ATTEMPT_RETENTION_MS = 5 * 60_000;
7
+ function normalizeAccountKey(value) {
8
+ if (value == null)
9
+ return DEFAULT_ACCOUNT;
10
+ if (typeof value !== "string")
11
+ throw new Error("account must be a string");
12
+ const account = value.trim();
13
+ const hasControlCharacter = [...account].some((character) => {
14
+ const code = character.charCodeAt(0);
15
+ return code <= 31 || code === 127;
16
+ });
17
+ if (!account || account.length > 128 || hasControlCharacter) {
18
+ throw new Error("account must be 1-128 characters without control characters");
19
+ }
20
+ return account;
21
+ }
22
+ function credentialKey(provider, accountKey) {
23
+ if (accountKey === DEFAULT_ACCOUNT)
24
+ return provider;
25
+ return `${ACCOUNT_STORAGE_PREFIX}${Buffer.from(JSON.stringify([provider, accountKey])).toString("base64url")}`;
26
+ }
27
+ function accountFromCredentialKey(provider, key) {
28
+ if (key === provider)
29
+ return DEFAULT_ACCOUNT;
30
+ if (!key.startsWith(ACCOUNT_STORAGE_PREFIX))
31
+ return null;
32
+ try {
33
+ const parsed = JSON.parse(Buffer.from(key.slice(ACCOUNT_STORAGE_PREFIX.length), "base64url").toString("utf8"));
34
+ return Array.isArray(parsed) && parsed[0] === provider && typeof parsed[1] === "string"
35
+ ? parsed[1]
36
+ : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function session(provider, accountKey, credential) {
43
+ return credential
44
+ ? {
45
+ provider,
46
+ accountKey,
47
+ authenticated: credential.metadata?.reauthRequired !== true,
48
+ reauthRequired: credential.metadata?.reauthRequired === true,
49
+ expiresAt: credential.expiresAt,
50
+ needsRefresh: credential.expiresAt <= Date.now(),
51
+ account: credential.account,
52
+ }
53
+ : { provider, accountKey, authenticated: false };
54
+ }
55
+ export class SubscriptionAuth {
56
+ store;
57
+ adapters = new Map();
58
+ attempts = new Map();
59
+ generations = new Map();
60
+ refreshes = new Map();
61
+ usageCache = new Map();
62
+ modelsCache = new Map();
63
+ usageInflight = new Map();
64
+ modelsInflight = new Map();
65
+ metadataGenerations = new Map();
66
+ refreshTimeoutMs;
67
+ usageCacheTtlMs;
68
+ modelsCacheTtlMs;
69
+ constructor(store, providers, options = {}) {
70
+ this.store = store;
71
+ this.refreshTimeoutMs = options.refreshTimeoutMs ?? 30_000;
72
+ this.usageCacheTtlMs = options.usageCacheTtlMs ?? 15_000;
73
+ this.modelsCacheTtlMs = options.modelsCacheTtlMs ?? 5 * 60_000;
74
+ for (const [name, value] of Object.entries({
75
+ refreshTimeoutMs: this.refreshTimeoutMs,
76
+ usageCacheTtlMs: this.usageCacheTtlMs,
77
+ modelsCacheTtlMs: this.modelsCacheTtlMs,
78
+ })) {
79
+ if (!Number.isFinite(value) || value <= 0)
80
+ throw new Error(`${name} must be greater than zero`);
81
+ }
82
+ for (const provider of providers) {
83
+ if (this.adapters.has(provider.id))
84
+ throw new Error(`Duplicate provider: ${provider.id}`);
85
+ this.adapters.set(provider.id, provider);
86
+ }
87
+ }
88
+ listProviders() {
89
+ return [...this.adapters.values()].map(({ id, name, loginModes, description, homepage, allowedHosts, loginFields, supportsFetch, proxyBaseUrl, proxy, getUsage, getModels, }) => ({
90
+ id,
91
+ name,
92
+ loginModes,
93
+ description,
94
+ homepage,
95
+ allowedHosts,
96
+ loginFields,
97
+ supportsFetch: supportsFetch !== false,
98
+ supportsProxy: Boolean(proxyBaseUrl || proxy),
99
+ supportsUsage: Boolean(getUsage),
100
+ supportsModels: Boolean(getModels),
101
+ }));
102
+ }
103
+ adapter(provider) {
104
+ const adapter = this.adapters.get(provider);
105
+ if (!adapter)
106
+ throw new Error(`Unknown subscription provider: ${provider}`);
107
+ return adapter;
108
+ }
109
+ generation(scope) {
110
+ return this.generations.get(scope) ?? 0;
111
+ }
112
+ advance(scope) {
113
+ const next = this.generation(scope) + 1;
114
+ this.generations.set(scope, next);
115
+ return next;
116
+ }
117
+ clearMetadata(scope) {
118
+ this.usageCache.delete(scope);
119
+ this.modelsCache.delete(scope);
120
+ this.usageInflight.delete(scope);
121
+ this.modelsInflight.delete(scope);
122
+ this.metadataGenerations.set(scope, (this.metadataGenerations.get(scope) ?? 0) + 1);
123
+ }
124
+ cachedMetadata(cache, inflight, scope, ttlMs, callerSignal, load) {
125
+ const cached = cache.get(scope);
126
+ if (cached && cached.expiresAt > Date.now())
127
+ return Promise.resolve(cached.value);
128
+ if (!callerSignal) {
129
+ const pending = inflight.get(scope);
130
+ if (pending)
131
+ return pending;
132
+ }
133
+ const generation = this.metadataGenerations.get(scope) ?? 0;
134
+ let request;
135
+ request = load()
136
+ .then((value) => {
137
+ if ((this.metadataGenerations.get(scope) ?? 0) === generation) {
138
+ cache.set(scope, { value, expiresAt: Date.now() + ttlMs });
139
+ }
140
+ return value;
141
+ })
142
+ .finally(() => {
143
+ if (inflight.get(scope) === request)
144
+ inflight.delete(scope);
145
+ });
146
+ if (!callerSignal)
147
+ inflight.set(scope, request);
148
+ return request;
149
+ }
150
+ async signIn(provider, options) {
151
+ const adapter = this.adapter(provider);
152
+ const accountKey = normalizeAccountKey(options?.account);
153
+ const scope = credentialKey(provider, accountKey);
154
+ const replace = options?.replace !== false;
155
+ if (!replace && (await this.store.read(scope))) {
156
+ throw new Error(`Account name ${accountKey} is already connected for ${provider}`);
157
+ }
158
+ const epoch = this.advance(scope);
159
+ for (const attempt of this.attempts.values()) {
160
+ if (attempt.scope === scope && attempt.state === "pending")
161
+ attempt.abort.abort();
162
+ }
163
+ const abort = new AbortController();
164
+ const providerOptions = { ...options };
165
+ delete providerOptions.account;
166
+ delete providerOptions.replace;
167
+ const login = await adapter.startLogin(abort.signal, providerOptions);
168
+ const id = crypto.randomUUID();
169
+ const record = {};
170
+ const promise = login.complete
171
+ .then(async (credential) => {
172
+ const saved = await this.store.modify(scope, (current) => {
173
+ if (this.generation(scope) !== epoch)
174
+ return current;
175
+ if (current && !replace) {
176
+ throw new Error(`Account name ${accountKey} is already connected for ${provider}`);
177
+ }
178
+ return credential;
179
+ });
180
+ if (this.generation(scope) !== epoch || !saved)
181
+ throw new Error("Login cancelled");
182
+ this.clearMetadata(scope);
183
+ record.state = "complete";
184
+ return session(provider, accountKey, saved);
185
+ })
186
+ .catch((error) => {
187
+ record.state = abort.signal.aborted ? "cancelled" : "failed";
188
+ record.error = errorMessage(error);
189
+ throw error;
190
+ });
191
+ void promise.catch(() => { });
192
+ Object.assign(record, {
193
+ id,
194
+ provider,
195
+ accountKey,
196
+ scope,
197
+ state: "pending",
198
+ error: null,
199
+ abort,
200
+ promise,
201
+ });
202
+ this.attempts.set(id, record);
203
+ void promise.then(() => this.expireLoginAttempt(id), () => this.expireLoginAttempt(id));
204
+ return {
205
+ id,
206
+ provider,
207
+ accountKey,
208
+ prompt: login.prompt,
209
+ get state() {
210
+ return record.state;
211
+ },
212
+ get error() {
213
+ return record.error;
214
+ },
215
+ wait: () => record.promise,
216
+ cancel: () => {
217
+ this.advance(scope);
218
+ abort.abort();
219
+ },
220
+ };
221
+ }
222
+ expireLoginAttempt(id) {
223
+ const timer = setTimeout(() => this.attempts.delete(id), LOGIN_ATTEMPT_RETENTION_MS);
224
+ timer.unref();
225
+ }
226
+ getLoginAttempt(id) {
227
+ const attempt = this.attempts.get(id);
228
+ return attempt
229
+ ? {
230
+ provider: attempt.provider,
231
+ accountKey: attempt.accountKey,
232
+ state: attempt.state,
233
+ error: attempt.error,
234
+ }
235
+ : null;
236
+ }
237
+ cancelLoginAttempt(id) {
238
+ const attempt = this.attempts.get(id);
239
+ if (!attempt || attempt.state !== "pending")
240
+ return false;
241
+ this.advance(attempt.scope);
242
+ attempt.abort.abort();
243
+ return true;
244
+ }
245
+ async status(provider, options = {}) {
246
+ const accountKey = normalizeAccountKey(options.account);
247
+ if (options.validate)
248
+ await this.getAccessToken(provider, accountKey).catch(() => null);
249
+ return session(provider, accountKey, await this.store.read(credentialKey(provider, accountKey)));
250
+ }
251
+ async statuses() {
252
+ const sessions = await Promise.all([...this.adapters.keys()].map(async (provider) => {
253
+ const accounts = await this.listAccounts(provider);
254
+ return accounts.length ? accounts : [await this.status(provider)];
255
+ }));
256
+ return sessions.flat();
257
+ }
258
+ async listAccounts(provider) {
259
+ this.adapter(provider);
260
+ const keys = this.store.listKeys ? await this.store.listKeys() : [provider];
261
+ const accounts = keys.flatMap((key) => {
262
+ const account = accountFromCredentialKey(provider, key);
263
+ return account == null ? [] : [account];
264
+ });
265
+ return Promise.all([...new Set(accounts)].map((account) => this.status(provider, { account })));
266
+ }
267
+ async signOut(provider, account = DEFAULT_ACCOUNT) {
268
+ const accountKey = normalizeAccountKey(account);
269
+ const scope = credentialKey(provider, accountKey);
270
+ this.advance(scope);
271
+ this.clearMetadata(scope);
272
+ for (const refresh of this.refreshes.get(scope) ?? [])
273
+ refresh.abort();
274
+ for (const attempt of this.attempts.values()) {
275
+ if (attempt.scope === scope && attempt.state === "pending")
276
+ attempt.abort.abort();
277
+ }
278
+ await this.store.delete(scope);
279
+ }
280
+ async credential(provider, account, forceRefresh = false) {
281
+ const adapter = this.adapter(provider);
282
+ const accountKey = normalizeAccountKey(account);
283
+ const scope = credentialKey(provider, accountKey);
284
+ const observed = await this.store.read(scope);
285
+ if (!observed)
286
+ throw new Error(`Not authenticated with ${provider} account ${accountKey}`);
287
+ if (observed.metadata?.reauthRequired === true) {
288
+ throw new Error(`Session expired for ${provider} account ${accountKey}; sign in again`);
289
+ }
290
+ if (!forceRefresh && observed.expiresAt > Date.now())
291
+ return observed;
292
+ const epoch = this.generation(scope);
293
+ const refreshed = await this.store.modify(scope, async (current) => {
294
+ if (!current)
295
+ throw new Error(`Not authenticated with ${provider} account ${accountKey}`);
296
+ if (this.generation(scope) !== epoch)
297
+ return current;
298
+ if (current.accessToken !== observed.accessToken && current.expiresAt > Date.now()) {
299
+ return current;
300
+ }
301
+ const abort = new AbortController();
302
+ const active = this.refreshes.get(scope) ?? new Set();
303
+ active.add(abort);
304
+ this.refreshes.set(scope, active);
305
+ try {
306
+ const signal = AbortSignal.any([abort.signal, AbortSignal.timeout(this.refreshTimeoutMs)]);
307
+ const next = await adapter.refresh(current, signal);
308
+ return this.generation(scope) === epoch ? next : current;
309
+ }
310
+ catch (error) {
311
+ if (adapter.isPermanentRefreshError?.(error)) {
312
+ return {
313
+ accessToken: "",
314
+ expiresAt: 0,
315
+ account: current.account,
316
+ metadata: { ...current.metadata, reauthRequired: true },
317
+ };
318
+ }
319
+ throw error;
320
+ }
321
+ finally {
322
+ active.delete(abort);
323
+ if (active.size === 0)
324
+ this.refreshes.delete(scope);
325
+ }
326
+ });
327
+ if (this.generation(scope) !== epoch) {
328
+ throw new Error(`Session changed while refreshing ${provider} account ${accountKey}`);
329
+ }
330
+ if (!refreshed || refreshed.metadata?.reauthRequired === true) {
331
+ this.clearMetadata(scope);
332
+ throw new Error(`Session expired for ${provider} account ${accountKey}; sign in again`);
333
+ }
334
+ this.clearMetadata(scope);
335
+ return refreshed;
336
+ }
337
+ async getAccessToken(provider, account = DEFAULT_ACCOUNT) {
338
+ const credential = await this.credential(provider, account);
339
+ if (credential.metadata?.delegatedCli === true) {
340
+ throw new Error(`${provider} authentication is delegated to its official CLI`);
341
+ }
342
+ return credential.accessToken;
343
+ }
344
+ async credentialSummary(provider, account = DEFAULT_ACCOUNT) {
345
+ const adapter = this.adapter(provider);
346
+ const accountKey = normalizeAccountKey(account);
347
+ const credential = await this.store.read(credentialKey(provider, accountKey));
348
+ if (!credential)
349
+ throw new Error(`Not authenticated with ${provider} account ${accountKey}`);
350
+ return {
351
+ provider,
352
+ accountKey,
353
+ accessCredentialStored: Boolean(credential.accessToken) && credential.metadata?.delegatedCli !== true,
354
+ refreshCredentialStored: Boolean(credential.refreshToken),
355
+ automaticRefresh: Boolean(credential.refreshToken) || credential.metadata?.delegatedCli === true,
356
+ expiresAt: credential.expiresAt,
357
+ needsRefresh: credential.expiresAt <= Date.now(),
358
+ externallyManaged: credential.metadata?.delegatedCli === true,
359
+ reauthRequired: credential.metadata?.reauthRequired === true,
360
+ endpoint: typeof adapter.proxyBaseUrl === "function"
361
+ ? adapter.proxyBaseUrl(credential)
362
+ : adapter.proxyBaseUrl,
363
+ account: credential.account,
364
+ };
365
+ }
366
+ async details(provider, account = DEFAULT_ACCOUNT, signal) {
367
+ const accountKey = normalizeAccountKey(account);
368
+ const [session, credential, usage, models] = await Promise.all([
369
+ this.status(provider, { account: accountKey }),
370
+ this.credentialSummary(provider, accountKey),
371
+ this.getUsage(provider, accountKey, signal),
372
+ this.getModels(provider, accountKey, signal),
373
+ ]);
374
+ return { session, credential, usage, models };
375
+ }
376
+ async fetch(provider, input, init, account = DEFAULT_ACCOUNT) {
377
+ const adapter = this.adapter(provider);
378
+ const original = new Request(input, init);
379
+ const accountKey = normalizeAccountKey(account);
380
+ const send = async (credential) => {
381
+ const authorized = await adapter.authorize(original.clone(), credential);
382
+ const headers = new Headers(authorized.headers);
383
+ headers.set("cache-control", "no-store");
384
+ const request = new Request(authorized, { cache: "no-store", headers });
385
+ const response = await globalThis.fetch(request);
386
+ return adapter.normalizeResponse?.(request, response) ?? response;
387
+ };
388
+ if (adapter.supportsFetch === false) {
389
+ throw new Error(`${provider} does not expose credentials for direct provider requests`);
390
+ }
391
+ let response = await send(await this.credential(provider, accountKey));
392
+ if (response.status === 401) {
393
+ await response.body?.cancel();
394
+ response = await send(await this.credential(provider, accountKey, true));
395
+ }
396
+ return response;
397
+ }
398
+ async proxy(provider, account, path, init) {
399
+ const adapter = this.adapter(provider);
400
+ const accountKey = normalizeAccountKey(account);
401
+ const credential = await this.credential(provider, accountKey);
402
+ if (adapter.proxy) {
403
+ const local = new Request(`http://aisubs.local/${path.replace(/^\//, "")}`, init);
404
+ let response = await adapter.proxy(local.clone(), credential);
405
+ if (response.status === 401) {
406
+ await response.body?.cancel();
407
+ response = await adapter.proxy(local.clone(), await this.credential(provider, accountKey, true));
408
+ }
409
+ return response;
410
+ }
411
+ const baseUrl = typeof adapter.proxyBaseUrl === "function"
412
+ ? adapter.proxyBaseUrl(credential)
413
+ : adapter.proxyBaseUrl;
414
+ if (!baseUrl)
415
+ throw new Error(`${provider} does not expose a direct API endpoint`);
416
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
417
+ return this.fetch(provider, new URL(path, base), init, accountKey);
418
+ }
419
+ async getUsage(provider, account = DEFAULT_ACCOUNT, callerSignal) {
420
+ const adapter = this.adapter(provider);
421
+ if (!adapter.getUsage)
422
+ return null;
423
+ const accountKey = normalizeAccountKey(account);
424
+ const scope = credentialKey(provider, accountKey);
425
+ return this.cachedMetadata(this.usageCache, this.usageInflight, scope, this.usageCacheTtlMs, callerSignal, async () => {
426
+ const credential = await this.credential(provider, accountKey);
427
+ const timeout = AbortSignal.timeout(this.refreshTimeoutMs);
428
+ const signal = callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout;
429
+ const data = await adapter.getUsage({
430
+ credential,
431
+ signal,
432
+ fetch: (input, init) => this.fetch(provider, input, init, accountKey),
433
+ });
434
+ return data ? { provider, accountKey, asOf: Date.now(), ...data } : null;
435
+ });
436
+ }
437
+ async getModels(provider, account = DEFAULT_ACCOUNT, callerSignal) {
438
+ const adapter = this.adapter(provider);
439
+ if (!adapter.getModels)
440
+ return null;
441
+ const accountKey = normalizeAccountKey(account);
442
+ const scope = credentialKey(provider, accountKey);
443
+ return this.cachedMetadata(this.modelsCache, this.modelsInflight, scope, this.modelsCacheTtlMs, callerSignal, async () => {
444
+ const credential = await this.credential(provider, accountKey);
445
+ const timeout = AbortSignal.timeout(this.refreshTimeoutMs);
446
+ const signal = callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout;
447
+ const models = await adapter.getModels({
448
+ credential,
449
+ signal,
450
+ fetch: (input, init) => this.fetch(provider, input, init, accountKey),
451
+ });
452
+ return { provider, accountKey, asOf: Date.now(), models };
453
+ });
454
+ }
455
+ account(provider, account) {
456
+ const accountKey = normalizeAccountKey(account);
457
+ this.adapter(provider);
458
+ return {
459
+ provider,
460
+ accountKey,
461
+ signIn: (options) => this.signIn(provider, { ...options, account: accountKey }),
462
+ status: (options) => this.status(provider, { ...options, account: accountKey }),
463
+ signOut: () => this.signOut(provider, accountKey),
464
+ getAccessToken: () => this.getAccessToken(provider, accountKey),
465
+ fetch: (input, init) => this.fetch(provider, input, init, accountKey),
466
+ proxy: (path, init) => this.proxy(provider, accountKey, path, init),
467
+ getUsage: (signal) => this.getUsage(provider, accountKey, signal),
468
+ getModels: (signal) => this.getModels(provider, accountKey, signal),
469
+ credentialSummary: () => this.credentialSummary(provider, accountKey),
470
+ details: (signal) => this.details(provider, accountKey, signal),
471
+ };
472
+ }
473
+ }
474
+ export function createSubscriptionAuth(options) {
475
+ const store = options.store ?? new FileCredentialStore(join(defaultAiSubsDataDir(), "credentials.json"));
476
+ return new SubscriptionAuth(store, options.providers, options);
477
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { join, resolve } from "node:path";
4
+ import { createSubscriptionAuth } from "./auth.js";
5
+ import { createSubscriptionAuthDashboardServer } from "./dashboard.js";
6
+ import { chatGptProvider } from "./providers/chatgpt.js";
7
+ import { claudeProvider } from "./providers/claude.js";
8
+ import { copilotProvider } from "./providers/copilot.js";
9
+ import { grokProvider } from "./providers/grok.js";
10
+ import { openCodeGoProvider, openCodeZenProvider } from "./providers/opencode.js";
11
+ import { defaultAiSubsDataDir, FileCredentialStore } from "./store.js";
12
+ const DEFAULT_DASHBOARD_PORT = 4319;
13
+ function usage() {
14
+ console.log(`AI Subs
15
+
16
+ Usage:
17
+ aisubs dashboard [options]
18
+
19
+ Options:
20
+ --data-dir <path> State directory (default: ~/.aisubs)
21
+ --port <number> Local port (default: 4319; use 0 for any available port)
22
+ --no-open Print the secure link without opening a browser
23
+ --help Show this help
24
+ `);
25
+ }
26
+ function value(args, index, flag) {
27
+ const next = args[index + 1];
28
+ if (!next || next.startsWith("--"))
29
+ throw new Error(`${flag} requires a value`);
30
+ return next;
31
+ }
32
+ function openBrowser(url) {
33
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
34
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
35
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
36
+ child.on("error", () => undefined);
37
+ child.unref();
38
+ }
39
+ async function main() {
40
+ const args = process.argv.slice(2);
41
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
42
+ usage();
43
+ return;
44
+ }
45
+ const command = args[0];
46
+ if (command !== "dashboard")
47
+ throw new Error(`Unknown command: ${command}`);
48
+ let dataDirectory = defaultAiSubsDataDir();
49
+ let port = DEFAULT_DASHBOARD_PORT;
50
+ let shouldOpen = true;
51
+ for (let index = 1; index < args.length; index += 1) {
52
+ const argument = args[index];
53
+ if (argument === "--no-open")
54
+ shouldOpen = false;
55
+ else if (argument === "--data-dir")
56
+ dataDirectory = resolve(value(args, index++, argument));
57
+ else if (argument === "--port")
58
+ port = Number(value(args, index++, argument));
59
+ else
60
+ throw new Error(`Unknown option: ${argument}`);
61
+ }
62
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
63
+ throw new Error("Invalid port");
64
+ const store = new FileCredentialStore(join(dataDirectory, "credentials.json"));
65
+ const auth = createSubscriptionAuth({
66
+ store,
67
+ providers: [
68
+ chatGptProvider(),
69
+ claudeProvider(),
70
+ copilotProvider(),
71
+ grokProvider(),
72
+ openCodeGoProvider(),
73
+ openCodeZenProvider(),
74
+ ],
75
+ });
76
+ const dashboard = await createSubscriptionAuthDashboardServer({ auth, port });
77
+ console.log(`AI Subs is running at ${dashboard.url}`);
78
+ console.log(`Credentials: ${store.file}`);
79
+ console.log(`Control API key: ${dashboard.apiKey}`);
80
+ console.log(`Dashboard link: ${dashboard.bootstrapUrl}`);
81
+ console.log("Press Ctrl+C to stop.");
82
+ if (shouldOpen)
83
+ openBrowser(dashboard.bootstrapUrl);
84
+ let closing = false;
85
+ const close = async () => {
86
+ if (closing)
87
+ return;
88
+ closing = true;
89
+ await dashboard.close();
90
+ };
91
+ process.once("SIGINT", () => void close().then(() => process.exit(0)));
92
+ process.once("SIGTERM", () => void close().then(() => process.exit(0)));
93
+ }
94
+ main().catch((error) => {
95
+ console.error(error instanceof Error ? error.message : String(error));
96
+ process.exitCode = 1;
97
+ });
@@ -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>