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/usage.js ADDED
@@ -0,0 +1,418 @@
1
+ import { isRecord, numberValue, stringValue } from "./utils.js";
2
+ function numeric(value) {
3
+ if (typeof value === "string" && value.trim()) {
4
+ const parsed = Number(value);
5
+ return Number.isFinite(parsed) ? parsed : undefined;
6
+ }
7
+ return numberValue(value);
8
+ }
9
+ function timestamp(value) {
10
+ const number = numeric(value);
11
+ if (number != null)
12
+ return number > 1_000_000_000_000 ? number : number * 1000;
13
+ const text = stringValue(value);
14
+ if (!text)
15
+ return undefined;
16
+ const parsed = Date.parse(text);
17
+ return Number.isNaN(parsed) ? undefined : parsed;
18
+ }
19
+ function rounded(value) {
20
+ return value == null ? undefined : Math.round(value * 10_000) / 10_000;
21
+ }
22
+ function title(value) {
23
+ return value
24
+ .replace(/^USAGE_PRODUCT_TYPE_/, "")
25
+ .replace(/^PRODUCT_/, "")
26
+ .replace(/[_-]+/g, " ")
27
+ .toLowerCase()
28
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
29
+ }
30
+ function grokPlan(value) {
31
+ const plan = value.trim();
32
+ const known = {
33
+ free: "Free",
34
+ grokpro: "SuperGrok",
35
+ supergrok: "SuperGrok",
36
+ supergrokpro: "SuperGrok Heavy",
37
+ supergrokheavy: "SuperGrok Heavy",
38
+ supergroklite: "SuperGrok Lite",
39
+ supergrokplus: "SuperGrok Plus",
40
+ xbasic: "X Basic",
41
+ xpremium: "X Premium",
42
+ xpremiumplus: "X Premium+",
43
+ };
44
+ return (known[plan
45
+ .replace(/\+/g, "plus")
46
+ .replace(/[^a-z0-9]/gi, "")
47
+ .toLowerCase()] ?? title(plan.replace(/([a-z0-9])([A-Z])/g, "$1 $2")));
48
+ }
49
+ function windowLabel(seconds) {
50
+ if (!seconds || seconds <= 0)
51
+ return "Limit";
52
+ if (seconds % 604_800 === 0) {
53
+ const weeks = seconds / 604_800;
54
+ return weeks === 1 ? "Weekly" : `${weeks}-week limit`;
55
+ }
56
+ if (seconds >= 86_400)
57
+ return `${Math.round(seconds / 86_400)}-day limit`;
58
+ if (seconds >= 3_600)
59
+ return `${Math.round(seconds / 3_600)}-hour limit`;
60
+ return `${Math.round(seconds / 60)}-min limit`;
61
+ }
62
+ function parseChatGptResetCredits(raw) {
63
+ if (!isRecord(raw))
64
+ return undefined;
65
+ const container = isRecord(raw.rate_limit_reset_credits)
66
+ ? raw.rate_limit_reset_credits
67
+ : isRecord(raw.rateLimitResetCredits)
68
+ ? raw.rateLimitResetCredits
69
+ : raw;
70
+ const availableCount = numeric(container.available_count) ??
71
+ numeric(container.availableCount) ??
72
+ numeric(container.count);
73
+ if (availableCount == null)
74
+ return undefined;
75
+ const credits = (Array.isArray(container.credits) ? container.credits : []).flatMap((item) => {
76
+ if (!isRecord(item))
77
+ return [];
78
+ return [
79
+ {
80
+ id: stringValue(item.id),
81
+ status: stringValue(item.status),
82
+ grantedAt: timestamp(item.granted_at ?? item.grantedAt),
83
+ expiresAt: timestamp(item.expires_at ?? item.expiresAt ?? item.expiry_at ?? item.expiryAt),
84
+ },
85
+ ];
86
+ });
87
+ return {
88
+ availableCount: Math.max(0, Math.floor(availableCount)),
89
+ credits: credits.length ? credits : undefined,
90
+ };
91
+ }
92
+ export function parseChatGptUsage(raw, detailedResetCredits) {
93
+ if (!isRecord(raw))
94
+ return null;
95
+ const rateLimit = isRecord(raw.rate_limit) ? raw.rate_limit : null;
96
+ const meters = [];
97
+ if (rateLimit) {
98
+ for (const [id, value] of [
99
+ ["primary", rateLimit.primary_window],
100
+ ["secondary", rateLimit.secondary_window],
101
+ ]) {
102
+ if (!isRecord(value))
103
+ continue;
104
+ const percentUsed = numeric(value.used_percent);
105
+ if (percentUsed == null)
106
+ continue;
107
+ const resetAt = timestamp(value.reset_at);
108
+ const resetAfter = numeric(value.reset_after_seconds);
109
+ meters.push({
110
+ id,
111
+ label: windowLabel(numeric(value.limit_window_seconds)),
112
+ unit: "percent",
113
+ percentUsed,
114
+ resetAt: resetAt ?? (resetAfter ? Date.now() + resetAfter * 1000 : undefined),
115
+ });
116
+ }
117
+ }
118
+ const additional = Array.isArray(raw.additional_rate_limits) ? raw.additional_rate_limits : [];
119
+ for (const item of additional) {
120
+ if (!isRecord(item) || !isRecord(item.rate_limit))
121
+ continue;
122
+ const group = stringValue(item.limit_name) ?? stringValue(item.metered_feature) ?? "Additional";
123
+ for (const [windowId, value] of [
124
+ ["primary", item.rate_limit.primary_window],
125
+ ["secondary", item.rate_limit.secondary_window],
126
+ ]) {
127
+ if (!isRecord(value))
128
+ continue;
129
+ const percentUsed = numeric(value.used_percent);
130
+ if (percentUsed == null)
131
+ continue;
132
+ const resetAt = timestamp(value.reset_at);
133
+ const resetAfter = numeric(value.reset_after_seconds);
134
+ meters.push({
135
+ id: `${stringValue(item.metered_feature) ?? group}:${windowId}`,
136
+ label: `${title(group)} / ${windowLabel(numeric(value.limit_window_seconds))}`,
137
+ unit: "percent",
138
+ percentUsed,
139
+ resetAt: resetAt ?? (resetAfter ? Date.now() + resetAfter * 1000 : undefined),
140
+ });
141
+ }
142
+ }
143
+ const resetCredits = parseChatGptResetCredits(detailedResetCredits) ??
144
+ parseChatGptResetCredits(raw.rate_limit_reset_credits ?? raw.rateLimitResetCredits);
145
+ if (resetCredits) {
146
+ meters.push({
147
+ id: "reset-credits",
148
+ label: "Reset credits",
149
+ unit: "credits",
150
+ remaining: resetCredits.availableCount,
151
+ });
152
+ }
153
+ const plan = stringValue(raw.plan_type);
154
+ const accountId = stringValue(raw.account_id) ?? stringValue(raw.user_id);
155
+ const email = stringValue(raw.email);
156
+ const creditStatus = isRecord(raw.credits) ? raw.credits : null;
157
+ const facts = [];
158
+ if (creditStatus?.unlimited === true)
159
+ facts.push({ label: "Additional credits", value: "Unlimited" });
160
+ else if (creditStatus?.has_credits === true) {
161
+ const balance = stringValue(creditStatus.balance);
162
+ if (balance)
163
+ facts.push({ label: "Credit balance", value: balance });
164
+ }
165
+ if (!meters.length && !facts.length)
166
+ return null;
167
+ const displayPlan = plan ? title(plan) : undefined;
168
+ return {
169
+ plan: displayPlan,
170
+ account: accountId || email || displayPlan
171
+ ? { id: accountId, label: email, email, plan: displayPlan }
172
+ : undefined,
173
+ meters: meters.length ? meters : undefined,
174
+ facts: facts.length ? facts : undefined,
175
+ resetCredits,
176
+ };
177
+ }
178
+ const COPILOT_LABELS = {
179
+ premium_interactions: "Premium requests",
180
+ chat: "Chat credits",
181
+ completions: "Inline suggestions",
182
+ };
183
+ export function copilotPlanName(raw) {
184
+ if (!isRecord(raw))
185
+ return undefined;
186
+ const sku = stringValue(raw.access_type_sku)?.toLowerCase() ?? "";
187
+ const plan = stringValue(raw.copilot_plan)?.toLowerCase() ?? "";
188
+ if (sku === "free_limited_copilot")
189
+ return "Free";
190
+ if (sku === "free_educational_quota" || plan === "individual_edu")
191
+ return "Student";
192
+ if (sku.includes("enterprise") || plan === "enterprise")
193
+ return "Enterprise";
194
+ if (sku.includes("business") || sku.includes("standalone_seat") || plan === "business") {
195
+ return "Business";
196
+ }
197
+ if (sku.includes("max") || plan === "individual_max")
198
+ return "Max";
199
+ if (sku.includes("plus") || plan === "individual_pro")
200
+ return "Pro+";
201
+ if (sku.includes("subscriber") || sku.includes("individual") || plan === "individual") {
202
+ return "Pro";
203
+ }
204
+ return plan ? title(plan) : sku ? title(sku) : undefined;
205
+ }
206
+ function copilotMeter(id, snapshot, resetAt) {
207
+ const label = COPILOT_LABELS[id] ?? title(id);
208
+ const limit = numeric(snapshot.entitlement);
209
+ const unlimited = snapshot.unlimited === true || limit === -1;
210
+ const included = unlimited || (snapshot.has_quota !== false && (limit == null || limit > 0));
211
+ if (!included)
212
+ return { id, label, unit: "requests", included: false, resetAt };
213
+ if (unlimited) {
214
+ return { id, label, unit: "requests", included: true, unlimited: true, resetAt };
215
+ }
216
+ const remaining = numeric(snapshot.quota_remaining) ?? numeric(snapshot.remaining);
217
+ const providerUsed = numeric(snapshot.credits_used);
218
+ const used = providerUsed ??
219
+ (limit != null && remaining != null ? Math.max(0, limit - remaining) : undefined);
220
+ const percentRemaining = numeric(snapshot.percent_remaining);
221
+ const percentUsed = percentRemaining != null
222
+ ? 100 - percentRemaining
223
+ : limit != null && limit > 0 && used != null
224
+ ? (used / limit) * 100
225
+ : undefined;
226
+ return {
227
+ id,
228
+ label,
229
+ unit: id === "chat" ? "credits" : "requests",
230
+ used: rounded(used),
231
+ limit,
232
+ remaining: rounded(remaining),
233
+ percentUsed: rounded(percentUsed),
234
+ included: true,
235
+ resetAt,
236
+ };
237
+ }
238
+ export function parseCopilotUsage(raw) {
239
+ if (!isRecord(raw))
240
+ return null;
241
+ const resetAt = timestamp(raw.quota_reset_date_utc) ??
242
+ timestamp(raw.quota_reset_date) ??
243
+ timestamp(raw.limited_user_reset_date);
244
+ const meters = [];
245
+ if (isRecord(raw.quota_snapshots)) {
246
+ for (const [id, snapshot] of Object.entries(raw.quota_snapshots)) {
247
+ if (isRecord(snapshot))
248
+ meters.push(copilotMeter(id, snapshot, resetAt));
249
+ }
250
+ }
251
+ else if (isRecord(raw.limited_user_quotas) && isRecord(raw.monthly_quotas)) {
252
+ for (const [id, remaining] of Object.entries(raw.limited_user_quotas)) {
253
+ const limit = numeric(raw.monthly_quotas[id]);
254
+ const quotaRemaining = numeric(remaining);
255
+ if (limit == null || quotaRemaining == null)
256
+ continue;
257
+ meters.push(copilotMeter(id, { entitlement: limit, quota_remaining: quotaRemaining }, resetAt));
258
+ }
259
+ }
260
+ const plan = copilotPlanName(raw);
261
+ const login = stringValue(raw.login);
262
+ const hasOverage = isRecord(raw.quota_snapshots)
263
+ ? Object.values(raw.quota_snapshots).some((snapshot) => isRecord(snapshot) && snapshot.overage_permitted === true)
264
+ : false;
265
+ const facts = [
266
+ ...(raw.token_based_billing === true
267
+ ? [{ label: "Usage accounting", value: "AI credits" }]
268
+ : []),
269
+ ...(hasOverage ? [{ label: "Extra usage", value: "Enabled" }] : []),
270
+ ];
271
+ return {
272
+ plan,
273
+ account: login || plan ? { id: login, label: login, plan } : undefined,
274
+ meters: meters.length ? meters : undefined,
275
+ facts: facts.length ? facts : undefined,
276
+ note: meters.length ? undefined : "No quota details were returned for this plan.",
277
+ };
278
+ }
279
+ function wrappedNumeric(value) {
280
+ return isRecord(value) ? numeric(value.val) : numeric(value);
281
+ }
282
+ export function parseGrokUsage(raw, userRaw, settingsRaw) {
283
+ if (!isRecord(raw) || !isRecord(raw.config))
284
+ return null;
285
+ const config = raw.config;
286
+ const period = isRecord(config.currentPeriod) ? config.currentPeriod : null;
287
+ const resetAt = timestamp(period?.end) ?? timestamp(config.billingPeriodEnd);
288
+ const periodType = stringValue(period?.type);
289
+ const percentUsed = numeric(config.creditUsagePercent);
290
+ const meters = [];
291
+ if (period || percentUsed != null) {
292
+ meters.push({
293
+ id: "period",
294
+ label: periodType?.includes("WEEKLY") ? "Weekly" : "Usage period",
295
+ unit: "percent",
296
+ percentUsed,
297
+ resetAt,
298
+ window: periodType,
299
+ });
300
+ }
301
+ else {
302
+ const legacyLimit = wrappedNumeric(config.monthlyLimit);
303
+ const legacyUsed = wrappedNumeric(config.used);
304
+ if (legacyLimit != null && legacyLimit > 0) {
305
+ meters.push({
306
+ id: "period",
307
+ label: "Monthly",
308
+ unit: "currency",
309
+ used: Math.max(0, legacyUsed ?? 0) / 100,
310
+ limit: legacyLimit / 100,
311
+ remaining: Math.max(0, legacyLimit - (legacyUsed ?? 0)) / 100,
312
+ percentUsed: rounded(((legacyUsed ?? 0) / legacyLimit) * 100),
313
+ resetAt,
314
+ });
315
+ }
316
+ }
317
+ const products = Array.isArray(config.productUsage) ? config.productUsage : [];
318
+ for (const product of products) {
319
+ if (!isRecord(product))
320
+ continue;
321
+ const id = stringValue(product.product) ??
322
+ stringValue(product.productType) ??
323
+ stringValue(product.name) ??
324
+ stringValue(product.type);
325
+ const productPercent = numeric(product.creditUsagePercent) ??
326
+ numeric(product.usagePercent) ??
327
+ numeric(product.usedPercent) ??
328
+ numeric(product.percent);
329
+ if (id && productPercent != null) {
330
+ meters.push({
331
+ id: id.toLowerCase(),
332
+ label: title(id),
333
+ unit: "percent",
334
+ percentUsed: productPercent,
335
+ resetAt,
336
+ });
337
+ }
338
+ }
339
+ const extraLimit = wrappedNumeric(config.onDemandCap);
340
+ const extraUsed = wrappedNumeric(config.onDemandUsed);
341
+ if (extraLimit != null && extraLimit > 0) {
342
+ meters.push({
343
+ id: "extra-usage",
344
+ label: "Extra usage",
345
+ unit: "currency",
346
+ used: Math.max(0, extraUsed ?? 0) / 100,
347
+ limit: extraLimit / 100,
348
+ remaining: Math.max(0, extraLimit - (extraUsed ?? 0)) / 100,
349
+ resetAt,
350
+ });
351
+ }
352
+ const user = isRecord(userRaw) ? userRaw : null;
353
+ const settings = isRecord(settingsRaw) ? settingsRaw : null;
354
+ const liveTier = stringValue(user?.subscriptionTier) ?? stringValue(user?.subscription_tier);
355
+ const fallbackTier = stringValue(settings?.subscription_tier_display) ??
356
+ stringValue(settings?.subscriptionTierDisplay) ??
357
+ stringValue(raw.subscriptionTier) ??
358
+ stringValue(raw.subscription_tier) ??
359
+ stringValue(settings?.subscription_tier) ??
360
+ stringValue(settings?.subscriptionTier) ??
361
+ stringValue(config.subscriptionTier);
362
+ const prepaid = wrappedNumeric(config.prepaidBalance);
363
+ const unified = config.isUnifiedBillingUser === true || config.is_unified_billing_user === true;
364
+ const onDemandEnabled = typeof raw.onDemandEnabled === "boolean"
365
+ ? raw.onDemandEnabled
366
+ : typeof raw.on_demand_enabled === "boolean"
367
+ ? raw.on_demand_enabled
368
+ : undefined;
369
+ const userId = stringValue(user?.id) ?? stringValue(user?.userId) ?? stringValue(user?.user_id);
370
+ const email = stringValue(user?.email);
371
+ const fullName = [stringValue(user?.firstName), stringValue(user?.lastName)]
372
+ .filter(Boolean)
373
+ .join(" ");
374
+ const label = stringValue(user?.name) ?? stringValue(user?.username) ?? (fullName || email);
375
+ const plan = liveTier ? grokPlan(liveTier) : user ? "Free" : grokPlan(fallbackTier ?? "Free");
376
+ const buildAccess = typeof user?.hasGrokCodeAccess === "boolean"
377
+ ? user.hasGrokCodeAccess
378
+ : typeof settings?.allow_access === "boolean"
379
+ ? settings.allow_access
380
+ : typeof settings?.allowAccess === "boolean"
381
+ ? settings.allowAccess
382
+ : undefined;
383
+ const team = stringValue(user?.teamName);
384
+ const organization = stringValue(user?.organizationName);
385
+ const retentionOptOut = typeof user?.codingDataRetentionOptOut === "boolean"
386
+ ? user.codingDataRetentionOptOut
387
+ : undefined;
388
+ return {
389
+ plan,
390
+ account: userId || label || email || plan ? { id: userId, label, email, plan } : undefined,
391
+ meters: meters.length ? meters : undefined,
392
+ facts: [
393
+ ...(buildAccess == null
394
+ ? []
395
+ : [{ label: "Grok Build access", value: buildAccess ? "Included" : "Not included" }]),
396
+ ...(team ? [{ label: "Team", value: team }] : []),
397
+ ...(organization ? [{ label: "Organization", value: organization }] : []),
398
+ ...(retentionOptOut == null
399
+ ? []
400
+ : [
401
+ {
402
+ label: "Coding data training",
403
+ value: retentionOptOut ? "Opted out" : "Allowed",
404
+ },
405
+ ]),
406
+ ...(prepaid == null
407
+ ? []
408
+ : [{ label: "Extra credits balance", value: `$${(prepaid / 100).toFixed(2)}` }]),
409
+ ...(onDemandEnabled == null
410
+ ? []
411
+ : [{ label: "Extra usage", value: onDemandEnabled ? "Enabled" : "Not enabled" }]),
412
+ ...(unified ? [{ label: "Usage pool", value: "Shared across Grok products" }] : []),
413
+ ],
414
+ note: percentUsed == null
415
+ ? "For this Free account, xAI provides only the reset time, not current usage or remaining allowance. Access may stop before the reset if the included allowance is exhausted."
416
+ : undefined,
417
+ };
418
+ }
@@ -0,0 +1,11 @@
1
+ import type { OAuthCredential } from "./types.js";
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ export declare function stringValue(value: unknown): string | undefined;
4
+ export declare function numberValue(value: unknown): number | undefined;
5
+ export declare function stringArray(value: unknown): string[] | undefined;
6
+ export declare function errorMessage(error: unknown): string;
7
+ export declare function abortableDelay(ms: number, signal?: AbortSignal): Promise<void>;
8
+ export declare function responseJson(response: Response, label: string): Promise<Record<string, unknown>>;
9
+ export declare function requireAllowedHost(request: Request, hosts: readonly (string | RegExp)[]): void;
10
+ export declare function bearerRequest(request: Request, credential: OAuthCredential, extraHeaders?: Record<string, string>): Request;
11
+ export declare function urlHost(host: string): string;
package/dist/utils.js ADDED
@@ -0,0 +1,68 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ export function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ export function stringValue(value) {
6
+ return typeof value === "string" && value ? value : undefined;
7
+ }
8
+ export function numberValue(value) {
9
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
10
+ }
11
+ export function stringArray(value) {
12
+ if (!Array.isArray(value))
13
+ return undefined;
14
+ const values = value.filter((item) => typeof item === "string");
15
+ return values.length ? values : undefined;
16
+ }
17
+ export function errorMessage(error) {
18
+ return error instanceof Error ? error.message : String(error);
19
+ }
20
+ export async function abortableDelay(ms, signal) {
21
+ try {
22
+ await delay(ms, undefined, { signal });
23
+ }
24
+ catch {
25
+ throw new Error("Login cancelled");
26
+ }
27
+ }
28
+ export async function responseJson(response, label) {
29
+ const raw = await response.json().catch(() => null);
30
+ if (!response.ok) {
31
+ const detail = isRecord(raw)
32
+ ? (stringValue(raw.error_description) ?? stringValue(raw.error) ?? stringValue(raw.message))
33
+ : undefined;
34
+ throw new Error(`${label} failed (${response.status})${detail ? `: ${detail}` : ""}`);
35
+ }
36
+ if (!isRecord(raw))
37
+ throw new Error(`${label} returned invalid JSON`);
38
+ return raw;
39
+ }
40
+ export function requireAllowedHost(request, hosts) {
41
+ const url = new URL(request.url);
42
+ if (url.protocol !== "https:") {
43
+ throw new Error(`Refusing to send subscription credentials over ${url.protocol}`);
44
+ }
45
+ const host = url.hostname;
46
+ if (!hosts.some((allowed) => (typeof allowed === "string" ? host === allowed : allowed.test(host)))) {
47
+ throw new Error(`Refusing to send subscription credentials to ${host}`);
48
+ }
49
+ }
50
+ export function bearerRequest(request, credential, extraHeaders) {
51
+ const headers = new Headers(request.headers);
52
+ for (const name of [
53
+ "authorization",
54
+ "cookie",
55
+ "proxy-authorization",
56
+ "x-api-key",
57
+ "x-goog-api-key",
58
+ ]) {
59
+ headers.delete(name);
60
+ }
61
+ headers.set("authorization", `Bearer ${credential.accessToken}`);
62
+ for (const [name, value] of Object.entries(extraHeaders ?? {}))
63
+ headers.set(name, value);
64
+ return new Request(request, { headers });
65
+ }
66
+ export function urlHost(host) {
67
+ return host.includes(":") ? `[${host}]` : host;
68
+ }
@@ -0,0 +1,38 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import {
4
+ FileCredentialStore,
5
+ chatGptProvider,
6
+ copilotProvider,
7
+ createSubscriptionAuth,
8
+ grokProvider,
9
+ } from "aisubs";
10
+
11
+ const provider = process.argv[2] ?? "chatgpt";
12
+ const accountKey = process.argv[3] ?? "default";
13
+ if (!["chatgpt", "copilot", "grok"].includes(provider)) {
14
+ throw new Error("Use chatgpt, copilot, or grok");
15
+ }
16
+
17
+ const auth = createSubscriptionAuth({
18
+ store: new FileCredentialStore(join(homedir(), ".aisubs-demo", "credentials.json")),
19
+ providers: [chatGptProvider(), copilotProvider(), grokProvider()],
20
+ });
21
+ const account = auth.account(provider, accountKey);
22
+
23
+ if (!(await account.status()).authenticated) {
24
+ const login = await account.signIn();
25
+ if (login.prompt.mode === "browser") {
26
+ console.log(`Open ${login.prompt.authorizationUri}`);
27
+ } else if (login.prompt.mode === "device") {
28
+ console.log(`Open ${login.prompt.verificationUri}`);
29
+ console.log(`Enter code: ${login.prompt.userCode}`);
30
+ }
31
+ await login.wait();
32
+ console.log("Signed in; refresh credentials were saved securely.");
33
+ }
34
+
35
+ // In-process request: no localhost server, port, CORS, or control API key.
36
+ const response = await account.proxy("models");
37
+ console.log(`Provider response: ${response.status}`);
38
+ console.log((await response.text()).slice(0, 1000));
@@ -0,0 +1,20 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ FileCredentialStore,
6
+ chatGptProvider,
7
+ copilotProvider,
8
+ createSubscriptionAuth,
9
+ grokProvider,
10
+ } from "aisubs";
11
+ import { createSubscriptionAuthServer } from "aisubs/http";
12
+
13
+ const apiKey = process.env.AISUBS_API_KEY ?? randomBytes(24).toString("hex");
14
+ const auth = createSubscriptionAuth({
15
+ store: new FileCredentialStore(join(homedir(), ".aisubs-demo", "credentials.json")),
16
+ providers: [chatGptProvider(), copilotProvider(), grokProvider()],
17
+ });
18
+ const server = await createSubscriptionAuthServer({ auth, apiKey, port: 4319 });
19
+ console.log(`AI Subs API: ${server.url}`);
20
+ console.log(`API key: ${apiKey}`);
package/package.json ADDED
@@ -0,0 +1,122 @@
1
+ {
2
+ "name": "aisubs",
3
+ "version": "0.1.0",
4
+ "description": "Connect AI provider accounts and use those subscriptions from any local tool or as api.",
5
+ "keywords": [
6
+ "ai",
7
+ "ai-subscription-manager",
8
+ "ai-subscription-to-api",
9
+ "ai-subscriptions",
10
+ "ai-subtoapi",
11
+ "anthropic",
12
+ "chatgpt",
13
+ "github-copilot",
14
+ "grok",
15
+ "grok-build",
16
+ "llm",
17
+ "oauth",
18
+ "openai",
19
+ "opencode"
20
+ ],
21
+ "homepage": "https://github.com/MpMeetPatel/aisubs#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/MpMeetPatel/aisubs/issues"
24
+ },
25
+ "license": "MIT",
26
+ "author": {
27
+ "name": "meetpatel"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/MpMeetPatel/aisubs.git"
32
+ },
33
+ "bin": {
34
+ "aisubs": "./dist/cli.js"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "dashboard/public/aisubs-mark.svg",
39
+ "examples",
40
+ "public",
41
+ "README.md"
42
+ ],
43
+ "type": "module",
44
+ "sideEffects": false,
45
+ "main": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
47
+ "exports": {
48
+ ".": {
49
+ "types": "./dist/index.d.ts",
50
+ "import": "./dist/index.js"
51
+ },
52
+ "./http": {
53
+ "types": "./dist/http.d.ts",
54
+ "import": "./dist/http.js"
55
+ },
56
+ "./dashboard": {
57
+ "types": "./dist/dashboard.d.ts",
58
+ "import": "./dist/dashboard.js"
59
+ },
60
+ "./providers/chatgpt": {
61
+ "types": "./dist/providers/chatgpt.d.ts",
62
+ "import": "./dist/providers/chatgpt.js"
63
+ },
64
+ "./providers/claude": {
65
+ "types": "./dist/providers/claude.d.ts",
66
+ "import": "./dist/providers/claude.js"
67
+ },
68
+ "./providers/copilot": {
69
+ "types": "./dist/providers/copilot.d.ts",
70
+ "import": "./dist/providers/copilot.js"
71
+ },
72
+ "./providers/grok": {
73
+ "types": "./dist/providers/grok.d.ts",
74
+ "import": "./dist/providers/grok.js"
75
+ },
76
+ "./providers/opencode": {
77
+ "types": "./dist/providers/opencode.d.ts",
78
+ "import": "./dist/providers/opencode.js"
79
+ }
80
+ },
81
+ "publishConfig": {
82
+ "access": "public"
83
+ },
84
+ "scripts": {
85
+ "dev": "node --run build && node scripts/dev.mjs",
86
+ "prepare": "node --run build",
87
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p dashboard/tsconfig.json --noEmit && vite build --config dashboard/vite.config.ts",
88
+ "prepack": "node --run check",
89
+ "test": "vitest run",
90
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p dashboard/tsconfig.json",
91
+ "lint": "oxlint .",
92
+ "fmt": "oxfmt .",
93
+ "fmt:check": "oxfmt --check .",
94
+ "check": "node --run typecheck && node --run lint && node --run fmt:check && node --run test && node --run build"
95
+ },
96
+ "devDependencies": {
97
+ "@tailwindcss/vite": "^4.3.3",
98
+ "@types/node": "^26.2.0",
99
+ "@types/react": "^19.2.18",
100
+ "@types/react-dom": "^19.2.4",
101
+ "@vitejs/plugin-react": "^6.0.5",
102
+ "lucide-react": "^1.31.0",
103
+ "oxfmt": "^0.63.0",
104
+ "oxlint": "^1.78.0",
105
+ "react": "^19.2.8",
106
+ "react-dom": "^19.2.8",
107
+ "tailwindcss": "^4.3.3",
108
+ "typescript": "^7.0.2",
109
+ "vite": "^8.2.1",
110
+ "vitest": "^4.1.10"
111
+ },
112
+ "devEngines": {
113
+ "packageManager": {
114
+ "name": "nub",
115
+ "version": "^0.6.0",
116
+ "onFail": "warn"
117
+ }
118
+ },
119
+ "engines": {
120
+ "node": ">=24"
121
+ }
122
+ }
Binary file