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
@@ -0,0 +1,289 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { bearerRequest, isRecord, numberValue, requireAllowedHost, responseJson, stringValue, } from "../utils.js";
4
+ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
5
+ const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
6
+ const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
7
+ const SCOPE = "user:inference user:profile user:sessions:claude_code user:mcp_servers user:file_upload";
8
+ const EXPIRY_SKEW_MS = 5 * 60_000;
9
+ const LOGIN_TIMEOUT_MS = 10 * 60_000;
10
+ const CLAUDE_CODE_SYSTEM = "You are Claude Code, Anthropic's official CLI for Claude.";
11
+ class ClaudeTokenError extends Error {
12
+ status;
13
+ code;
14
+ constructor(status, code) {
15
+ super(`Claude token exchange failed (${code ?? status})`);
16
+ this.status = status;
17
+ this.code = code;
18
+ }
19
+ }
20
+ function planLabel(value) {
21
+ const plan = stringValue(value);
22
+ return plan
23
+ ?.split(/[_-]/)
24
+ .filter(Boolean)
25
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
26
+ .join(" ");
27
+ }
28
+ function credentialFromTokens(raw, previous) {
29
+ const accessToken = stringValue(raw.access_token);
30
+ const refreshToken = stringValue(raw.refresh_token) ?? previous?.refreshToken;
31
+ if (!accessToken || !refreshToken)
32
+ throw new Error("Claude token response is incomplete");
33
+ const organization = isRecord(raw.organization) ? raw.organization : {};
34
+ const expiresAt = numberValue(raw.expires_at);
35
+ const expiresIn = numberValue(raw.expires_in);
36
+ return {
37
+ accessToken,
38
+ refreshToken,
39
+ expiresAt: expiresAt && expiresAt > Date.now()
40
+ ? expiresAt - EXPIRY_SKEW_MS
41
+ : Date.now() + Math.max(60, expiresIn ?? 8 * 60 * 60) * 1000 - EXPIRY_SKEW_MS,
42
+ account: {
43
+ id: stringValue(organization.uuid) ?? stringValue(organization.id) ?? previous?.account?.id,
44
+ label: stringValue(organization.name) ?? previous?.account?.label,
45
+ email: stringValue(raw.email) ?? previous?.account?.email,
46
+ plan: planLabel(raw.subscription_type) ?? previous?.account?.plan,
47
+ },
48
+ };
49
+ }
50
+ export function claudeProvider(options = {}) {
51
+ const clientId = options.clientId ?? CLIENT_ID;
52
+ const compatibilityVersion = options.compatibilityVersion ?? "2.1.231";
53
+ const fetcher = options.fetch ?? globalThis.fetch;
54
+ async function exchange(code, verifier, redirectUri, state, signal) {
55
+ const response = await fetcher(TOKEN_URL, {
56
+ method: "POST",
57
+ headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
58
+ body: new URLSearchParams({
59
+ grant_type: "authorization_code",
60
+ code,
61
+ client_id: clientId,
62
+ redirect_uri: redirectUri,
63
+ code_verifier: verifier,
64
+ state,
65
+ }),
66
+ signal,
67
+ });
68
+ if (!response.ok) {
69
+ const raw = await response.json().catch(() => null);
70
+ const record = isRecord(raw) ? raw : {};
71
+ throw new ClaudeTokenError(response.status, stringValue(record.error));
72
+ }
73
+ return credentialFromTokens(await responseJson(response, "Claude token exchange"));
74
+ }
75
+ return {
76
+ id: "claude",
77
+ name: "Claude",
78
+ description: "Claude Pro, Max, Team, or Enterprise subscription access with browser sign-in.",
79
+ homepage: "https://claude.ai",
80
+ allowedHosts: ["api.anthropic.com"],
81
+ proxyBaseUrl: "https://api.anthropic.com/v1",
82
+ loginModes: ["browser"],
83
+ async startLogin(signal) {
84
+ const verifier = randomBytes(32).toString("base64url");
85
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
86
+ const state = randomBytes(32).toString("base64url");
87
+ let finish;
88
+ let timer;
89
+ const server = createServer(async (request, response) => {
90
+ const callback = new URL(request.url ?? "/", "http://localhost");
91
+ if (callback.pathname !== "/callback") {
92
+ response.writeHead(404).end();
93
+ return;
94
+ }
95
+ if (callback.searchParams.get("state") !== state) {
96
+ response.writeHead(400, {
97
+ "content-type": "text/plain; charset=utf-8",
98
+ "cache-control": "no-store",
99
+ });
100
+ response.end("Claude sign-in failed. You can close this tab and return to AI Subs.");
101
+ return finish(new Error("Claude OAuth state did not match"));
102
+ }
103
+ const error = callback.searchParams.get("error");
104
+ const code = callback.searchParams.get("code");
105
+ if (error || !code) {
106
+ response.writeHead(400, {
107
+ "content-type": "text/plain; charset=utf-8",
108
+ "cache-control": "no-store",
109
+ });
110
+ response.end("Claude sign-in failed. You can close this tab and return to AI Subs.");
111
+ return finish(new Error(error ?? "Claude authorization code is missing"));
112
+ }
113
+ try {
114
+ const address = server.address();
115
+ if (!address || typeof address === "string")
116
+ throw new Error("Claude callback is unavailable");
117
+ const credential = await exchange(code, verifier, `http://localhost:${address.port}/callback`, state, signal);
118
+ response.writeHead(200, {
119
+ "content-type": "text/plain; charset=utf-8",
120
+ "cache-control": "no-store",
121
+ });
122
+ response.end("Claude connected. You can close this tab and return to AI Subs.");
123
+ finish(undefined, credential);
124
+ }
125
+ catch (cause) {
126
+ response.writeHead(400, {
127
+ "content-type": "text/plain; charset=utf-8",
128
+ "cache-control": "no-store",
129
+ });
130
+ response.end("Claude sign-in failed. You can close this tab and return to AI Subs.");
131
+ finish(cause);
132
+ }
133
+ });
134
+ const complete = new Promise((resolve, reject) => {
135
+ finish = (error, credential) => {
136
+ if (timer)
137
+ clearTimeout(timer);
138
+ signal.removeEventListener("abort", abort);
139
+ if (server.listening)
140
+ server.close();
141
+ if (error)
142
+ reject(error);
143
+ else
144
+ resolve(credential);
145
+ };
146
+ });
147
+ let settled = false;
148
+ const originalFinish = finish;
149
+ finish = (error, credential) => {
150
+ if (settled)
151
+ return;
152
+ settled = true;
153
+ originalFinish(error, credential);
154
+ };
155
+ const abort = () => finish(new Error("Claude browser authorization cancelled"));
156
+ signal.addEventListener("abort", abort, { once: true });
157
+ await new Promise((resolve, reject) => {
158
+ server.once("error", reject);
159
+ server.listen(0, "127.0.0.1", () => {
160
+ server.off("error", reject);
161
+ resolve();
162
+ });
163
+ });
164
+ const address = server.address();
165
+ if (!address || typeof address === "string")
166
+ throw new Error("Unable to start Claude callback");
167
+ const redirectUri = `http://localhost:${address.port}/callback`;
168
+ timer = setTimeout(() => finish(new Error("Claude browser authorization timed out")), LOGIN_TIMEOUT_MS);
169
+ timer.unref();
170
+ const authorization = new URL(AUTHORIZE_URL);
171
+ authorization.search = new URLSearchParams({
172
+ code: "true",
173
+ client_id: clientId,
174
+ response_type: "code",
175
+ redirect_uri: redirectUri,
176
+ scope: SCOPE,
177
+ code_challenge: challenge,
178
+ code_challenge_method: "S256",
179
+ state,
180
+ }).toString();
181
+ return {
182
+ prompt: {
183
+ mode: "browser",
184
+ authorizationUri: authorization.toString(),
185
+ expiresAt: Date.now() + LOGIN_TIMEOUT_MS,
186
+ },
187
+ complete,
188
+ };
189
+ },
190
+ async refresh(credential, signal) {
191
+ if (!credential.refreshToken)
192
+ throw new Error("Claude refresh token is missing");
193
+ const response = await fetcher(TOKEN_URL, {
194
+ method: "POST",
195
+ headers: {
196
+ accept: "application/json",
197
+ "content-type": "application/x-www-form-urlencoded",
198
+ },
199
+ body: new URLSearchParams({
200
+ grant_type: "refresh_token",
201
+ client_id: clientId,
202
+ refresh_token: credential.refreshToken,
203
+ }),
204
+ signal,
205
+ });
206
+ if (!response.ok) {
207
+ const raw = await response.json().catch(() => null);
208
+ const record = isRecord(raw) ? raw : {};
209
+ throw new ClaudeTokenError(response.status, stringValue(record.error));
210
+ }
211
+ return credentialFromTokens(await responseJson(response, "Claude token refresh"), credential);
212
+ },
213
+ async authorize(request, credential) {
214
+ requireAllowedHost(request, ["api.anthropic.com"]);
215
+ const raw = request.method === "POST" && new URL(request.url).pathname.endsWith("/messages")
216
+ ? await request
217
+ .clone()
218
+ .json()
219
+ .catch(() => null)
220
+ : null;
221
+ const beta = new Set([
222
+ "claude-code-20250219",
223
+ "oauth-2025-04-20",
224
+ ...(request.headers.get("anthropic-beta")?.split(",") ?? []),
225
+ ]);
226
+ const authorized = bearerRequest(request, credential, {
227
+ "anthropic-version": "2023-06-01",
228
+ "anthropic-beta": [...beta]
229
+ .map((value) => value.trim())
230
+ .filter(Boolean)
231
+ .join(","),
232
+ "user-agent": `claude-cli/${compatibilityVersion}`,
233
+ "x-app": "cli",
234
+ });
235
+ if (!isRecord(raw))
236
+ return authorized;
237
+ const system = raw.system;
238
+ if (typeof system === "string" && system.startsWith(CLAUDE_CODE_SYSTEM))
239
+ return authorized;
240
+ if (Array.isArray(system) &&
241
+ isRecord(system[0]) &&
242
+ stringValue(system[0].text) === CLAUDE_CODE_SYSTEM) {
243
+ return authorized;
244
+ }
245
+ const headers = new Headers(authorized.headers);
246
+ headers.delete("content-length");
247
+ const identity = { type: "text", text: CLAUDE_CODE_SYSTEM };
248
+ const body = {
249
+ ...raw,
250
+ system: Array.isArray(system)
251
+ ? [identity, ...system]
252
+ : typeof system === "string"
253
+ ? `${CLAUDE_CODE_SYSTEM}\n\n${system}`
254
+ : [identity],
255
+ };
256
+ return new Request(authorized, { method: "POST", body: JSON.stringify(body), headers });
257
+ },
258
+ async getModels({ fetch, signal }) {
259
+ const raw = await responseJson(await fetch("https://api.anthropic.com/v1/models", {
260
+ headers: { accept: "application/json" },
261
+ signal,
262
+ }), "Claude models");
263
+ const values = Array.isArray(raw.data) ? raw.data : [];
264
+ return values.flatMap((value) => {
265
+ if (!isRecord(value))
266
+ return [];
267
+ const id = stringValue(value.id);
268
+ return id
269
+ ? [
270
+ {
271
+ id,
272
+ name: stringValue(value.display_name) ?? stringValue(value.name),
273
+ description: stringValue(value.description),
274
+ contextWindow: numberValue(value.context_window),
275
+ maxOutputTokens: numberValue(value.max_output_tokens),
276
+ endpoints: ["messages"],
277
+ available: true,
278
+ selectable: true,
279
+ },
280
+ ]
281
+ : [];
282
+ });
283
+ },
284
+ isPermanentRefreshError(error) {
285
+ return (error instanceof ClaudeTokenError &&
286
+ (error.status === 401 || error.status === 403 || error.code === "invalid_grant"));
287
+ },
288
+ };
289
+ }
@@ -0,0 +1,6 @@
1
+ import type { ProviderAdapter } from "../types.js";
2
+ export interface CopilotProviderOptions {
3
+ clientId?: string;
4
+ fetch?: typeof globalThis.fetch;
5
+ }
6
+ export declare function copilotProvider(options?: CopilotProviderOptions): ProviderAdapter;