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.
- package/LICENSE +21 -0
- package/README.md +629 -0
- package/dashboard/public/aisubs-mark.svg +11 -0
- package/dist/account-key.d.ts +2 -0
- package/dist/account-key.js +11 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +477 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +97 -0
- package/dist/dashboard/aisubs-mark.svg +11 -0
- package/dist/dashboard/assets/index-BMoILzPw.js +64 -0
- package/dist/dashboard/assets/index-DHqDdNVe.css +2 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/dashboard/logos/anthropic.svg +3 -0
- package/dist/dashboard/logos/github-copilot.svg +3 -0
- package/dist/dashboard/logos/google.svg +3 -0
- package/dist/dashboard/logos/openai.svg +3 -0
- package/dist/dashboard/logos/opencode-dark.svg +18 -0
- package/dist/dashboard/logos/opencode-light.svg +18 -0
- package/dist/dashboard/logos/xai.svg +3 -0
- package/dist/dashboard.d.ts +18 -0
- package/dist/dashboard.js +139 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.js +263 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/providers/chatgpt.d.ts +7 -0
- package/dist/providers/chatgpt.js +402 -0
- package/dist/providers/claude.d.ts +7 -0
- package/dist/providers/claude.js +289 -0
- package/dist/providers/copilot.d.ts +6 -0
- package/dist/providers/copilot.js +466 -0
- package/dist/providers/grok.d.ts +7 -0
- package/dist/providers/grok.js +215 -0
- package/dist/providers/opencode.d.ts +4 -0
- package/dist/providers/opencode.js +147 -0
- package/dist/store.d.ts +18 -0
- package/dist/store.js +121 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +5 -0
- package/dist/usage.js +418 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +68 -0
- package/examples/direct.mjs +38 -0
- package/examples/server.mjs +20 -0
- package/package.json +122 -0
- package/public/aisubs-chatgpt-account.png +0 -0
- package/public/aisubs-copilot-account.png +0 -0
- package/public/aisubs-dashboard.png +0 -0
- 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
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
|
Binary file
|
|
Binary file
|