aisubs 0.1.0 → 0.2.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/README.md +381 -425
- package/dist/auth.js +7 -2
- package/dist/cli.js +10 -7
- package/dist/dashboard/assets/index-DYe3pr2a.css +2 -0
- package/dist/dashboard/assets/index-V-AoOW2F.js +83 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard.d.ts +1 -0
- package/dist/dashboard.js +86 -27
- package/dist/http.js +140 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/store.d.ts +6 -0
- package/dist/store.js +51 -2
- package/examples/server.mjs +6 -4
- package/package.json +1 -1
- package/public/aisubs-dashboard.png +0 -0
- package/dist/dashboard/assets/index-BMoILzPw.js +0 -64
- package/dist/dashboard/assets/index-DHqDdNVe.css +0 -2
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="color-scheme" content="light dark" />
|
|
7
7
|
<meta name="theme-color" content="#181818" />
|
|
8
8
|
<title>AI Subs</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-V-AoOW2F.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DYe3pr2a.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/dist/dashboard.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { SubscriptionAuth } from "./auth.js";
|
|
|
3
3
|
export interface SubscriptionAuthDashboardOptions {
|
|
4
4
|
auth: SubscriptionAuth;
|
|
5
5
|
apiKey?: string;
|
|
6
|
+
regenerateApiKey?: () => Promise<string>;
|
|
6
7
|
host?: string;
|
|
7
8
|
port?: number;
|
|
8
9
|
/** Maximum buffered proxy request body. Defaults to 10 MiB. */
|
package/dist/dashboard.js
CHANGED
|
@@ -37,35 +37,62 @@ function secure(response) {
|
|
|
37
37
|
response.setHeader("x-content-type-options", "nosniff");
|
|
38
38
|
response.setHeader("x-frame-options", "DENY");
|
|
39
39
|
}
|
|
40
|
+
function hostname(value) {
|
|
41
|
+
if (!value)
|
|
42
|
+
return undefined;
|
|
43
|
+
try {
|
|
44
|
+
return new URL(`http://${value}`).hostname.replace(/^\[|\]$/g, "");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
40
50
|
export async function createSubscriptionAuthDashboardServer(options) {
|
|
41
51
|
const host = options.host ?? "127.0.0.1";
|
|
42
52
|
if (!["127.0.0.1", "::1", "localhost"].includes(host)) {
|
|
43
53
|
throw new Error("The AI Subs dashboard may only bind to localhost");
|
|
44
54
|
}
|
|
45
55
|
const origin = `http://${urlHost(host)}`;
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
let apiKey = options.apiKey ?? `aisubs_${randomBytes(32).toString("base64url")}`;
|
|
57
|
+
let regeneratingApiKey;
|
|
48
58
|
const sessionToken = randomBytes(32).toString("base64url");
|
|
49
|
-
|
|
59
|
+
const requestLogs = [];
|
|
60
|
+
const logStreams = new Set();
|
|
61
|
+
let requestId = 0;
|
|
50
62
|
const server = createServer(async (request, response) => {
|
|
51
63
|
secure(response);
|
|
52
64
|
try {
|
|
65
|
+
if (hostname(request.headers.host) !== host) {
|
|
66
|
+
return sendJson(response, 421, { error: "Invalid local host" });
|
|
67
|
+
}
|
|
53
68
|
const url = new URL(request.url ?? "/", origin);
|
|
54
|
-
if (url.pathname
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
if (url.pathname.startsWith("/aisubs/")) {
|
|
70
|
+
const startedAt = performance.now();
|
|
71
|
+
response.once("finish", () => {
|
|
72
|
+
const entry = {
|
|
73
|
+
id: ++requestId,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
method: request.method ?? "GET",
|
|
76
|
+
path: url.pathname,
|
|
77
|
+
status: response.statusCode,
|
|
78
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
79
|
+
};
|
|
80
|
+
requestLogs.push(entry);
|
|
81
|
+
if (requestLogs.length > 200)
|
|
82
|
+
requestLogs.shift();
|
|
83
|
+
const event = `data: ${JSON.stringify(entry)}\n\n`;
|
|
84
|
+
for (const stream of logStreams)
|
|
85
|
+
stream.write(event);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (url.pathname === "/health") {
|
|
89
|
+
return sendJson(response, 200, { ok: true }, {
|
|
90
|
+
"x-aisubs-service": "aisubs",
|
|
91
|
+
"x-aisubs-pid": String(process.pid),
|
|
66
92
|
});
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
}
|
|
94
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
95
|
+
response.setHeader("set-cookie", `aisubs_session=${encodeURIComponent(sessionToken)}; HttpOnly; SameSite=Strict; Path=/`);
|
|
69
96
|
}
|
|
70
97
|
const bearer = request.headers.authorization?.startsWith("Bearer ")
|
|
71
98
|
? request.headers.authorization.slice(7)
|
|
@@ -75,16 +102,42 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
75
102
|
: request.headers["x-api-key"];
|
|
76
103
|
const bearerAuthenticated = sameSecret(bearer, apiKey) || sameSecret(headerKey, apiKey);
|
|
77
104
|
const cookieAuthenticated = sameSecret(cookie(request, "aisubs_session"), sessionToken);
|
|
78
|
-
|
|
105
|
+
const apiRoute = ["v1", "aisubs"].includes(routeSegments(url.pathname)[0] ?? "");
|
|
106
|
+
if (apiRoute && !bearerAuthenticated && !cookieAuthenticated) {
|
|
79
107
|
return sendJson(response, 401, { error: "Unauthorized" });
|
|
80
108
|
}
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
if (request.method === "GET" && url.pathname === "/v1/logs/stream") {
|
|
110
|
+
response.writeHead(200, {
|
|
111
|
+
"content-type": "text/event-stream",
|
|
112
|
+
"cache-control": "no-store",
|
|
113
|
+
connection: "keep-alive",
|
|
114
|
+
});
|
|
115
|
+
response.flushHeaders();
|
|
116
|
+
for (const entry of requestLogs)
|
|
117
|
+
response.write(`data: ${JSON.stringify(entry)}\n\n`);
|
|
118
|
+
logStreams.add(response);
|
|
119
|
+
request.once("close", () => logStreams.delete(response));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (apiRoute && cookieAuthenticated && !bearerAuthenticated) {
|
|
123
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(request.method ?? "GET") &&
|
|
124
|
+
request.headers.origin !== `http://${request.headers.host}`) {
|
|
125
|
+
return sendJson(response, 403, { error: "Cross-origin mutation blocked" });
|
|
126
|
+
}
|
|
127
|
+
if (request.method === "GET" && url.pathname === "/v1/api-key") {
|
|
128
|
+
return sendJson(response, 200, { apiKey });
|
|
129
|
+
}
|
|
130
|
+
if (request.method === "POST" && url.pathname === "/v1/api-key/regenerate") {
|
|
131
|
+
regeneratingApiKey ??= (options.regenerateApiKey
|
|
132
|
+
? options.regenerateApiKey()
|
|
133
|
+
: Promise.resolve(`aisubs_${randomBytes(32).toString("base64url")}`)).finally(() => {
|
|
134
|
+
regeneratingApiKey = undefined;
|
|
135
|
+
});
|
|
136
|
+
apiKey = await regeneratingApiKey;
|
|
137
|
+
return sendJson(response, 200, { apiKey });
|
|
138
|
+
}
|
|
86
139
|
}
|
|
87
|
-
if (
|
|
140
|
+
if (apiRoute) {
|
|
88
141
|
if (!(await handleSubscriptionAuthApi(options.auth, request, response, url, options.maxProxyBodyBytes))) {
|
|
89
142
|
sendJson(response, 404, { error: "Not found" });
|
|
90
143
|
}
|
|
@@ -131,9 +184,15 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
131
184
|
const url = `${origin}:${address.port}`;
|
|
132
185
|
return {
|
|
133
186
|
server,
|
|
134
|
-
apiKey
|
|
187
|
+
get apiKey() {
|
|
188
|
+
return apiKey;
|
|
189
|
+
},
|
|
135
190
|
url,
|
|
136
|
-
bootstrapUrl:
|
|
137
|
-
close: () =>
|
|
191
|
+
bootstrapUrl: url,
|
|
192
|
+
close: () => {
|
|
193
|
+
for (const stream of logStreams)
|
|
194
|
+
stream.end();
|
|
195
|
+
return new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
196
|
+
},
|
|
138
197
|
};
|
|
139
198
|
}
|
package/dist/http.js
CHANGED
|
@@ -70,9 +70,133 @@ export function sendJson(response, status, body, headers = {}) {
|
|
|
70
70
|
export function routeSegments(pathname) {
|
|
71
71
|
return pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
72
72
|
}
|
|
73
|
+
function chatCompletionRequest(body) {
|
|
74
|
+
const raw = JSON.parse(body.toString("utf8"));
|
|
75
|
+
if (!isRecord(raw) || !Array.isArray(raw.messages)) {
|
|
76
|
+
throw new Error("Chat Completions requires a messages array");
|
|
77
|
+
}
|
|
78
|
+
const model = stringValue(raw.model);
|
|
79
|
+
if (!model)
|
|
80
|
+
throw new Error("Chat Completions requires a model");
|
|
81
|
+
if (raw.stream === true) {
|
|
82
|
+
throw new Error("ChatGPT Chat Completions compatibility does not support streaming");
|
|
83
|
+
}
|
|
84
|
+
const instructions = [];
|
|
85
|
+
const input = [];
|
|
86
|
+
for (const message of raw.messages) {
|
|
87
|
+
if (!isRecord(message))
|
|
88
|
+
throw new Error("Chat Completions messages must be objects");
|
|
89
|
+
const role = stringValue(message.role);
|
|
90
|
+
const content = stringValue(message.content);
|
|
91
|
+
if (!role || content == null) {
|
|
92
|
+
throw new Error("ChatGPT compatibility supports text-only messages");
|
|
93
|
+
}
|
|
94
|
+
if (role === "system" || role === "developer")
|
|
95
|
+
instructions.push(content);
|
|
96
|
+
else if (role === "user" || role === "assistant") {
|
|
97
|
+
input.push({
|
|
98
|
+
role,
|
|
99
|
+
content: [{ type: role === "assistant" ? "output_text" : "input_text", text: content }],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
throw new Error(`Unsupported Chat Completions role: ${role}`);
|
|
104
|
+
}
|
|
105
|
+
const translated = { model, store: false, stream: true, input };
|
|
106
|
+
if (instructions.length)
|
|
107
|
+
translated.instructions = instructions.join("\n\n");
|
|
108
|
+
const maxOutputTokens = raw.max_completion_tokens ?? raw.max_tokens;
|
|
109
|
+
if (typeof maxOutputTokens === "number")
|
|
110
|
+
translated.max_output_tokens = maxOutputTokens;
|
|
111
|
+
if (isRecord(raw.response_format) &&
|
|
112
|
+
raw.response_format.type === "json_schema" &&
|
|
113
|
+
isRecord(raw.response_format.json_schema)) {
|
|
114
|
+
translated.text = { format: { type: "json_schema", ...raw.response_format.json_schema } };
|
|
115
|
+
}
|
|
116
|
+
return { model, body: JSON.stringify(translated) };
|
|
117
|
+
}
|
|
118
|
+
function responseText(raw) {
|
|
119
|
+
if (!isRecord(raw))
|
|
120
|
+
return undefined;
|
|
121
|
+
const direct = stringValue(raw.output_text);
|
|
122
|
+
if (direct != null)
|
|
123
|
+
return direct;
|
|
124
|
+
if (!Array.isArray(raw.output))
|
|
125
|
+
return undefined;
|
|
126
|
+
const text = raw.output.flatMap((item) => {
|
|
127
|
+
if (!isRecord(item) || !Array.isArray(item.content))
|
|
128
|
+
return [];
|
|
129
|
+
return item.content.flatMap((part) => isRecord(part) && part.type === "output_text" && stringValue(part.text) != null
|
|
130
|
+
? [stringValue(part.text)]
|
|
131
|
+
: []);
|
|
132
|
+
});
|
|
133
|
+
return text.length ? text.join("") : undefined;
|
|
134
|
+
}
|
|
135
|
+
async function chatCompletionResponse(upstream, model) {
|
|
136
|
+
if (!upstream.ok)
|
|
137
|
+
return upstream;
|
|
138
|
+
let completed;
|
|
139
|
+
let content = "";
|
|
140
|
+
const body = await upstream.text();
|
|
141
|
+
if (body.startsWith("event:") || body.startsWith("data:")) {
|
|
142
|
+
for (const line of body.split(/\r?\n/)) {
|
|
143
|
+
if (!line.startsWith("data:"))
|
|
144
|
+
continue;
|
|
145
|
+
const data = line.slice(5).trim();
|
|
146
|
+
if (!data || data === "[DONE]")
|
|
147
|
+
continue;
|
|
148
|
+
const event = JSON.parse(data);
|
|
149
|
+
if (!isRecord(event))
|
|
150
|
+
continue;
|
|
151
|
+
if (event.type === "response.output_text.delta")
|
|
152
|
+
content += stringValue(event.delta) ?? "";
|
|
153
|
+
if (event.type === "response.completed")
|
|
154
|
+
completed = event.response;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
completed = JSON.parse(body);
|
|
159
|
+
content ||= responseText(completed) ?? "";
|
|
160
|
+
const raw = isRecord(completed) ? completed : {};
|
|
161
|
+
const usage = isRecord(raw.usage)
|
|
162
|
+
? {
|
|
163
|
+
prompt_tokens: raw.usage.input_tokens,
|
|
164
|
+
completion_tokens: raw.usage.output_tokens,
|
|
165
|
+
total_tokens: raw.usage.total_tokens,
|
|
166
|
+
}
|
|
167
|
+
: undefined;
|
|
168
|
+
return Response.json({
|
|
169
|
+
id: stringValue(raw.id) ?? `chatcmpl_${crypto.randomUUID()}`,
|
|
170
|
+
object: "chat.completion",
|
|
171
|
+
created: typeof raw.created_at === "number" ? raw.created_at : Math.floor(Date.now() / 1000),
|
|
172
|
+
model: stringValue(raw.model) ?? model,
|
|
173
|
+
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
|
|
174
|
+
...(usage ? { usage } : {}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
73
177
|
export async function handleSubscriptionAuthApi(auth, request, response, url, maxProxyBodyBytes = MAX_PROXY_BODY_BYTES) {
|
|
74
178
|
const parts = routeSegments(url.pathname);
|
|
75
179
|
if (parts[0] === "aisubs" && parts[1] && parts[2] && request.method !== "OPTIONS") {
|
|
180
|
+
const upstreamParts = parts.slice(3);
|
|
181
|
+
const versioned = upstreamParts[0] === "v1";
|
|
182
|
+
if (versioned)
|
|
183
|
+
upstreamParts.shift();
|
|
184
|
+
if (versioned && upstreamParts[0] === "embeddings") {
|
|
185
|
+
sendJson(response, 404, { error: "AISubs does not expose an embeddings API" });
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (versioned && request.method === "GET" && upstreamParts.join("/") === "models") {
|
|
189
|
+
const catalog = await auth.getModels(parts[1], parts[2]);
|
|
190
|
+
sendJson(response, 200, {
|
|
191
|
+
object: "list",
|
|
192
|
+
data: (catalog?.models ?? []).map((model) => ({
|
|
193
|
+
id: model.id,
|
|
194
|
+
object: "model",
|
|
195
|
+
owned_by: parts[1],
|
|
196
|
+
})),
|
|
197
|
+
});
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
76
200
|
const headers = new Headers();
|
|
77
201
|
const privateHeaders = new Set([
|
|
78
202
|
"authorization",
|
|
@@ -95,13 +219,26 @@ export async function handleSubscriptionAuthApi(auth, request, response, url, ma
|
|
|
95
219
|
}
|
|
96
220
|
}
|
|
97
221
|
const body = await readProxyBody(request, maxProxyBodyBytes);
|
|
98
|
-
|
|
222
|
+
let path = upstreamParts.map(encodeURIComponent).join("/") + url.search;
|
|
223
|
+
let proxyBody = body;
|
|
224
|
+
let chatCompletionModel;
|
|
225
|
+
if (parts[1] === "chatgpt" &&
|
|
226
|
+
versioned &&
|
|
227
|
+
request.method === "POST" &&
|
|
228
|
+
upstreamParts.join("/") === "chat/completions") {
|
|
229
|
+
const translated = chatCompletionRequest(body);
|
|
230
|
+
path = "responses";
|
|
231
|
+
proxyBody = Buffer.from(translated.body);
|
|
232
|
+
chatCompletionModel = translated.model;
|
|
233
|
+
headers.set("accept", "text/event-stream");
|
|
234
|
+
headers.set("content-type", "application/json");
|
|
235
|
+
}
|
|
99
236
|
const upstream = await auth.proxy(parts[1], parts[2], path, {
|
|
100
237
|
method: request.method,
|
|
101
238
|
headers,
|
|
102
|
-
body:
|
|
239
|
+
body: proxyBody.length ? proxyBody : undefined,
|
|
103
240
|
});
|
|
104
|
-
await sendUpstream(response, upstream);
|
|
241
|
+
await sendUpstream(response, chatCompletionModel ? await chatCompletionResponse(upstream, chatCompletionModel) : upstream);
|
|
105
242
|
return true;
|
|
106
243
|
}
|
|
107
244
|
if (request.method === "GET" && parts.join("/") === "v1/providers") {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, type SubscriptionAccount, type SubscriptionAuthOptions, } from "./auth.js";
|
|
2
|
-
export { defaultAiSubsDataDir, FileCredentialStore, MemoryCredentialStore } from "./store.js";
|
|
2
|
+
export { defaultAiSubsDataDir, FileApiKeyStore, FileCredentialStore, MemoryCredentialStore, } from "./store.js";
|
|
3
3
|
export { chatGptProvider, type ChatGptProviderOptions } from "./providers/chatgpt.js";
|
|
4
4
|
export { claudeProvider, type ClaudeProviderOptions } from "./providers/claude.js";
|
|
5
5
|
export { copilotProvider, type CopilotProviderOptions } from "./providers/copilot.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, } from "./auth.js";
|
|
2
|
-
export { defaultAiSubsDataDir, FileCredentialStore, MemoryCredentialStore } from "./store.js";
|
|
2
|
+
export { defaultAiSubsDataDir, FileApiKeyStore, FileCredentialStore, MemoryCredentialStore, } from "./store.js";
|
|
3
3
|
export { chatGptProvider } from "./providers/chatgpt.js";
|
|
4
4
|
export { claudeProvider } from "./providers/claude.js";
|
|
5
5
|
export { copilotProvider } from "./providers/copilot.js";
|
package/dist/store.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { CredentialStore, OAuthCredential, ProviderId } from "./types.js";
|
|
2
2
|
export declare function defaultAiSubsDataDir(): string;
|
|
3
|
+
export declare class FileApiKeyStore {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
constructor(file: string);
|
|
6
|
+
readOrCreate(): Promise<string>;
|
|
7
|
+
regenerate(): Promise<string>;
|
|
8
|
+
}
|
|
3
9
|
export declare class FileCredentialStore implements CredentialStore {
|
|
4
10
|
readonly file: string;
|
|
5
11
|
constructor(file: string);
|
package/dist/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { chmod, link, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { abortableDelay, isRecord } from "./utils.js";
|
|
@@ -9,6 +9,55 @@ export function defaultAiSubsDataDir() {
|
|
|
9
9
|
const override = process.env.AISUBS_DATA_DIR?.trim();
|
|
10
10
|
return override ? resolve(override) : join(homedir(), ".aisubs");
|
|
11
11
|
}
|
|
12
|
+
export class FileApiKeyStore {
|
|
13
|
+
file;
|
|
14
|
+
constructor(file) {
|
|
15
|
+
this.file = file;
|
|
16
|
+
}
|
|
17
|
+
async readOrCreate() {
|
|
18
|
+
try {
|
|
19
|
+
const value = (await readFile(this.file, "utf8")).trim();
|
|
20
|
+
if (value)
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code !== "ENOENT")
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
const value = `aisubs_${randomBytes(32).toString("base64url")}`;
|
|
28
|
+
const temp = `${this.file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
29
|
+
await mkdir(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(temp, `${value}\n`, { mode: 0o600 });
|
|
32
|
+
try {
|
|
33
|
+
await link(temp, this.file);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code !== "EEXIST")
|
|
38
|
+
throw error;
|
|
39
|
+
const existing = (await readFile(this.file, "utf8")).trim();
|
|
40
|
+
return existing || this.regenerate();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await rm(temp, { force: true }).catch(() => { });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async regenerate() {
|
|
48
|
+
const value = `aisubs_${randomBytes(32).toString("base64url")}`;
|
|
49
|
+
const temp = `${this.file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
50
|
+
await mkdir(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
51
|
+
try {
|
|
52
|
+
await writeFile(temp, `${value}\n`, { mode: 0o600 });
|
|
53
|
+
await rename(temp, this.file);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await rm(temp, { force: true }).catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
12
61
|
async function readEnvelope(file) {
|
|
13
62
|
try {
|
|
14
63
|
const parsed = JSON.parse(await readFile(file, "utf8"));
|
package/examples/server.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import {
|
|
5
4
|
FileCredentialStore,
|
|
5
|
+
FileApiKeyStore,
|
|
6
6
|
chatGptProvider,
|
|
7
7
|
copilotProvider,
|
|
8
8
|
createSubscriptionAuth,
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
} from "aisubs";
|
|
11
11
|
import { createSubscriptionAuthServer } from "aisubs/http";
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const directory = join(homedir(), ".aisubs-demo");
|
|
14
|
+
const apiKey =
|
|
15
|
+
process.env.AISUBS_API_KEY ??
|
|
16
|
+
(await new FileApiKeyStore(join(directory, "api-key")).readOrCreate());
|
|
14
17
|
const auth = createSubscriptionAuth({
|
|
15
|
-
store: new FileCredentialStore(join(
|
|
18
|
+
store: new FileCredentialStore(join(directory, "credentials.json")),
|
|
16
19
|
providers: [chatGptProvider(), copilotProvider(), grokProvider()],
|
|
17
20
|
});
|
|
18
21
|
const server = await createSubscriptionAuthServer({ auth, apiKey, port: 4319 });
|
|
19
22
|
console.log(`AI Subs API: ${server.url}`);
|
|
20
|
-
console.log(`API key: ${apiKey}`);
|
package/package.json
CHANGED
|
Binary file
|