aisubs 0.2.0 → 0.3.1
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/CHANGELOG.md +29 -0
- package/README.md +177 -46
- package/dist/account-key.js +1 -1
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +21 -4
- package/dist/compatibility.d.ts +14 -0
- package/dist/compatibility.js +1515 -0
- package/dist/dashboard/assets/index-BJDbjHnw.css +2 -0
- package/dist/dashboard/assets/index-DJBtdmoj.js +84 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard.d.ts +4 -2
- package/dist/dashboard.js +209 -88
- package/dist/http.d.ts +9 -6
- package/dist/http.js +374 -278
- package/dist/providers/chatgpt.js +2 -0
- package/dist/providers/claude.js +2 -0
- package/dist/providers/copilot.js +20 -2
- package/dist/providers/grok.js +2 -2
- package/dist/realtime.d.ts +6 -0
- package/dist/realtime.js +165 -0
- package/dist/store.js +27 -1
- package/dist/usage.js +1 -1
- package/examples/direct.mjs +31 -8
- package/examples/server.mjs +11 -1
- package/package.json +11 -1
- package/public/aisubs-dashboard.png +0 -0
- package/scripts/codex-catalog.mjs +203 -0
- package/dist/dashboard/assets/index-DYe3pr2a.css +0 -2
- package/dist/dashboard/assets/index-V-AoOW2F.js +0 -83
|
@@ -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-DJBtdmoj.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BJDbjHnw.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/dist/dashboard.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type FastifyInstance } from "fastify";
|
|
2
|
+
import type { Server } from "node:http";
|
|
2
3
|
import type { SubscriptionAuth } from "./auth.js";
|
|
3
4
|
export interface SubscriptionAuthDashboardOptions {
|
|
4
5
|
auth: SubscriptionAuth;
|
|
@@ -6,11 +7,12 @@ export interface SubscriptionAuthDashboardOptions {
|
|
|
6
7
|
regenerateApiKey?: () => Promise<string>;
|
|
7
8
|
host?: string;
|
|
8
9
|
port?: number;
|
|
9
|
-
/** Maximum
|
|
10
|
+
/** Maximum request body accepted by the compatibility proxy. Defaults to 10 MiB. */
|
|
10
11
|
maxProxyBodyBytes?: number;
|
|
11
12
|
}
|
|
12
13
|
export interface SubscriptionAuthDashboardServer {
|
|
13
14
|
server: Server;
|
|
15
|
+
app: FastifyInstance;
|
|
14
16
|
apiKey: string;
|
|
15
17
|
url: string;
|
|
16
18
|
bootstrapUrl: string;
|
package/dist/dashboard.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import cors from "@fastify/cors";
|
|
2
|
+
import websocket from "@fastify/websocket";
|
|
3
|
+
import Fastify from "fastify";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
1
5
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
6
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
4
8
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
9
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { handleSubscriptionAuthApi, routeSegments,
|
|
7
|
-
import {
|
|
10
|
+
import { clientAbortSignal, handleSubscriptionAuthApi, routeSegments, sendWebResponse, } from "./http.js";
|
|
11
|
+
import { registerRealtimeProxy } from "./realtime.js";
|
|
12
|
+
import { errorMessage, isRecord, stringValue, urlHost } from "./utils.js";
|
|
8
13
|
const ASSET_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), "dashboard");
|
|
9
14
|
const CONTENT_TYPES = {
|
|
10
15
|
".css": "text/css; charset=utf-8",
|
|
@@ -20,6 +25,16 @@ function sameSecret(actual, expected) {
|
|
|
20
25
|
const right = Buffer.from(expected);
|
|
21
26
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
22
27
|
}
|
|
28
|
+
function requestApiKeys(request) {
|
|
29
|
+
const authorization = request.headers.authorization;
|
|
30
|
+
const bearer = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
|
|
31
|
+
const header = (name) => {
|
|
32
|
+
const value = request.headers[name];
|
|
33
|
+
return Array.isArray(value) ? value[0] : value;
|
|
34
|
+
};
|
|
35
|
+
const queryKey = new URL(request.url, "http://aisubs.local").searchParams.get("key") ?? undefined;
|
|
36
|
+
return [bearer, header("x-api-key"), header("x-goog-api-key"), queryKey].filter((value) => value != null);
|
|
37
|
+
}
|
|
23
38
|
function cookie(request, name) {
|
|
24
39
|
for (const part of request.headers.cookie?.split(";") ?? []) {
|
|
25
40
|
const [key, ...value] = part.trim().split("=");
|
|
@@ -28,14 +43,14 @@ function cookie(request, name) {
|
|
|
28
43
|
}
|
|
29
44
|
return undefined;
|
|
30
45
|
}
|
|
31
|
-
function secure(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
function secure(reply) {
|
|
47
|
+
reply.header("content-security-policy", "default-src 'self'; base-uri 'none'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'");
|
|
48
|
+
reply.header("referrer-policy", "no-referrer");
|
|
49
|
+
reply.header("cross-origin-opener-policy", "same-origin");
|
|
50
|
+
reply.header("cross-origin-resource-policy", "same-origin");
|
|
51
|
+
reply.header("permissions-policy", "camera=(), geolocation=(), microphone=()");
|
|
52
|
+
reply.header("x-content-type-options", "nosniff");
|
|
53
|
+
reply.header("x-frame-options", "DENY");
|
|
39
54
|
}
|
|
40
55
|
function hostname(value) {
|
|
41
56
|
if (!value)
|
|
@@ -47,6 +62,53 @@ function hostname(value) {
|
|
|
47
62
|
return undefined;
|
|
48
63
|
}
|
|
49
64
|
}
|
|
65
|
+
async function responseFailure(response) {
|
|
66
|
+
if (response.ok)
|
|
67
|
+
return undefined;
|
|
68
|
+
const body = await response
|
|
69
|
+
.clone()
|
|
70
|
+
.json()
|
|
71
|
+
.catch(() => null);
|
|
72
|
+
const failure = isRecord(body) ? body.error : undefined;
|
|
73
|
+
const message = isRecord(failure) ? stringValue(failure.message) : stringValue(failure);
|
|
74
|
+
return (message ?? `HTTP ${response.status}`).slice(0, 2_000);
|
|
75
|
+
}
|
|
76
|
+
function runCodexCatalog(env) {
|
|
77
|
+
const script = join(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "codex-catalog.mjs");
|
|
78
|
+
return new Promise((resolvePromise, reject) => {
|
|
79
|
+
const child = spawn(process.execPath, [script], { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
80
|
+
let output = "";
|
|
81
|
+
child.stdout.on("data", (chunk) => {
|
|
82
|
+
output += chunk.toString();
|
|
83
|
+
});
|
|
84
|
+
child.stderr.on("data", (chunk) => {
|
|
85
|
+
output += chunk.toString();
|
|
86
|
+
});
|
|
87
|
+
child.once("error", reject);
|
|
88
|
+
child.once("close", (code) => resolvePromise({ output: output.trim(), code: code ?? 1 }));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function removeRootSetting(config, pattern) {
|
|
92
|
+
const firstTable = config.search(/^\[/m);
|
|
93
|
+
const rootEnd = firstTable < 0 ? config.length : firstTable;
|
|
94
|
+
return `${config.slice(0, rootEnd).replace(pattern, "")}${config.slice(rootEnd)}`;
|
|
95
|
+
}
|
|
96
|
+
async function restoreOfficialCodexConfig() {
|
|
97
|
+
const path = process.env.CODEX_CONFIG ?? join(homedir(), ".codex", "config.toml");
|
|
98
|
+
const backup = `${path}.aisubs-backup`;
|
|
99
|
+
const config = await readFile(path, "utf8").catch(() => null);
|
|
100
|
+
if (!config) {
|
|
101
|
+
return "No existing Codex config found at ~/.codex/config.toml";
|
|
102
|
+
}
|
|
103
|
+
await access(backup).catch(() => writeFile(backup, config, { mode: 0o600 }));
|
|
104
|
+
let restored = removeRootSetting(config, /^model_catalog_json\s*=.*\n?/m);
|
|
105
|
+
restored = removeRootSetting(restored, /^model_provider\s*=\s*"aisubs-codex"\s*\n?/m);
|
|
106
|
+
restored = restored
|
|
107
|
+
.replace(/(?:^|\n)\[model_providers\.aisubs-codex\][\s\S]*?(?=\n\[|$)/, "")
|
|
108
|
+
.replace(/^AISUBS_API_KEY\s*=.*\n?/m, "");
|
|
109
|
+
await writeFile(path, restored, { mode: 0o600 });
|
|
110
|
+
return `Restored official Codex mode. Backup: ${backup}`;
|
|
111
|
+
}
|
|
50
112
|
export async function createSubscriptionAuthDashboardServer(options) {
|
|
51
113
|
const host = options.host ?? "127.0.0.1";
|
|
52
114
|
if (!["127.0.0.1", "::1", "localhost"].includes(host)) {
|
|
@@ -57,75 +119,133 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
57
119
|
let regeneratingApiKey;
|
|
58
120
|
const sessionToken = randomBytes(32).toString("base64url");
|
|
59
121
|
const requestLogs = [];
|
|
122
|
+
const requestErrors = new WeakMap();
|
|
60
123
|
const logStreams = new Set();
|
|
61
124
|
let requestId = 0;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
const app = Fastify({
|
|
126
|
+
bodyLimit: options.maxProxyBodyBytes ?? 10 * 1024 * 1024,
|
|
127
|
+
forceCloseConnections: true,
|
|
128
|
+
});
|
|
129
|
+
app.removeAllContentTypeParsers();
|
|
130
|
+
app.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => done(null, body));
|
|
131
|
+
await app.register(cors, {
|
|
132
|
+
origin: true,
|
|
133
|
+
credentials: false,
|
|
134
|
+
methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
135
|
+
allowedHeaders: [
|
|
136
|
+
"authorization",
|
|
137
|
+
"content-type",
|
|
138
|
+
"x-api-key",
|
|
139
|
+
"x-goog-api-key",
|
|
140
|
+
"anthropic-version",
|
|
141
|
+
"anthropic-beta",
|
|
142
|
+
"openai-beta",
|
|
143
|
+
],
|
|
144
|
+
exposedHeaders: ["x-request-id", "retry-after"],
|
|
145
|
+
});
|
|
146
|
+
await app.register(websocket);
|
|
147
|
+
await app.register(async (scope) => {
|
|
148
|
+
registerRealtimeProxy(scope, options.auth, (request) => {
|
|
149
|
+
return (requestApiKeys(request).some((value) => sameSecret(value, apiKey)) ||
|
|
150
|
+
sameSecret(cookie(request, "aisubs_session"), sessionToken));
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
154
|
+
secure(reply);
|
|
155
|
+
if (hostname(request.headers.host) !== host) {
|
|
156
|
+
await reply.code(421).send({ error: "Invalid local host" });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (request.url.startsWith("/aisubs/") || request.url.startsWith("/aisubs-codex/")) {
|
|
160
|
+
const startedAt = performance.now();
|
|
161
|
+
reply.raw.once("finish", () => {
|
|
162
|
+
const path = new URL(request.url, origin).pathname;
|
|
163
|
+
const entry = {
|
|
164
|
+
id: ++requestId,
|
|
165
|
+
timestamp: Date.now(),
|
|
166
|
+
method: request.method,
|
|
167
|
+
path,
|
|
168
|
+
status: reply.statusCode,
|
|
169
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
170
|
+
error: requestErrors.get(request),
|
|
171
|
+
};
|
|
172
|
+
requestLogs.push(entry);
|
|
173
|
+
if (requestLogs.length > 200)
|
|
174
|
+
requestLogs.shift();
|
|
175
|
+
const event = `data: ${JSON.stringify(entry)}\n\n`;
|
|
176
|
+
for (const stream of logStreams)
|
|
177
|
+
stream.write(event);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
app.route({
|
|
182
|
+
method: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
183
|
+
url: "/*",
|
|
184
|
+
async handler(request, reply) {
|
|
185
|
+
const url = new URL(request.url, origin);
|
|
88
186
|
if (url.pathname === "/health") {
|
|
89
|
-
|
|
90
|
-
"x-aisubs-service"
|
|
91
|
-
"x-aisubs-pid"
|
|
92
|
-
|
|
187
|
+
await reply
|
|
188
|
+
.header("x-aisubs-service", "aisubs")
|
|
189
|
+
.header("x-aisubs-pid", String(process.pid))
|
|
190
|
+
.send({ ok: true });
|
|
191
|
+
return;
|
|
93
192
|
}
|
|
94
193
|
if (request.method === "GET" && url.pathname === "/") {
|
|
95
|
-
|
|
194
|
+
reply.header("set-cookie", `aisubs_session=${encodeURIComponent(sessionToken)}; HttpOnly; SameSite=Strict; Path=/`);
|
|
96
195
|
}
|
|
97
|
-
const
|
|
98
|
-
? request.headers.authorization.slice(7)
|
|
99
|
-
: undefined;
|
|
100
|
-
const headerKey = Array.isArray(request.headers["x-api-key"])
|
|
101
|
-
? request.headers["x-api-key"][0]
|
|
102
|
-
: request.headers["x-api-key"];
|
|
103
|
-
const bearerAuthenticated = sameSecret(bearer, apiKey) || sameSecret(headerKey, apiKey);
|
|
196
|
+
const bearerAuthenticated = requestApiKeys(request).some((value) => sameSecret(value, apiKey));
|
|
104
197
|
const cookieAuthenticated = sameSecret(cookie(request, "aisubs_session"), sessionToken);
|
|
105
|
-
const apiRoute = ["v1", "aisubs"].includes(routeSegments(url.pathname)[0] ?? "");
|
|
198
|
+
const apiRoute = ["v1", "aisubs", "aisubs-codex"].includes(routeSegments(url.pathname)[0] ?? "");
|
|
106
199
|
if (apiRoute && !bearerAuthenticated && !cookieAuthenticated) {
|
|
107
|
-
|
|
200
|
+
await reply.code(401).send({
|
|
201
|
+
error: {
|
|
202
|
+
message: "Unauthorized",
|
|
203
|
+
type: "authentication_error",
|
|
204
|
+
code: "invalid_api_key",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
108
208
|
}
|
|
109
209
|
if (request.method === "GET" && url.pathname === "/v1/logs/stream") {
|
|
110
|
-
|
|
210
|
+
reply.hijack();
|
|
211
|
+
reply.raw.writeHead(200, {
|
|
111
212
|
"content-type": "text/event-stream",
|
|
112
213
|
"cache-control": "no-store",
|
|
113
214
|
connection: "keep-alive",
|
|
114
215
|
});
|
|
115
|
-
|
|
216
|
+
reply.raw.flushHeaders();
|
|
116
217
|
for (const entry of requestLogs)
|
|
117
|
-
|
|
118
|
-
logStreams.add(
|
|
119
|
-
request.once("close", () => logStreams.delete(
|
|
218
|
+
reply.raw.write(`data: ${JSON.stringify(entry)}\n\n`);
|
|
219
|
+
logStreams.add(reply.raw);
|
|
220
|
+
request.raw.once("close", () => logStreams.delete(reply.raw));
|
|
120
221
|
return;
|
|
121
222
|
}
|
|
122
223
|
if (apiRoute && cookieAuthenticated && !bearerAuthenticated) {
|
|
123
|
-
if (!["GET", "HEAD", "OPTIONS"].includes(request.method
|
|
224
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(request.method) &&
|
|
124
225
|
request.headers.origin !== `http://${request.headers.host}`) {
|
|
125
|
-
|
|
226
|
+
await reply.code(403).send({ error: "Cross-origin mutation blocked" });
|
|
227
|
+
return;
|
|
126
228
|
}
|
|
127
229
|
if (request.method === "GET" && url.pathname === "/v1/api-key") {
|
|
128
|
-
|
|
230
|
+
await reply.send({ apiKey });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (request.method === "POST" && url.pathname === "/v1/codex/configure") {
|
|
234
|
+
const result = await runCodexCatalog({
|
|
235
|
+
...process.env,
|
|
236
|
+
AISUBS_API_KEY: apiKey,
|
|
237
|
+
AISUBS_URL: `http://${request.headers.host ?? `${urlHost(host)}:${options.port ?? 4319}`}`,
|
|
238
|
+
});
|
|
239
|
+
if (result.code !== 0) {
|
|
240
|
+
await reply.code(500).send({ error: result.output || "Codex configuration failed" });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
await reply.send({ ok: true, output: result.output });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (request.method === "POST" && url.pathname === "/v1/codex/restore-official") {
|
|
247
|
+
await reply.send({ ok: true, output: await restoreOfficialCodexConfig() });
|
|
248
|
+
return;
|
|
129
249
|
}
|
|
130
250
|
if (request.method === "POST" && url.pathname === "/v1/api-key/regenerate") {
|
|
131
251
|
regeneratingApiKey ??= (options.regenerateApiKey
|
|
@@ -134,20 +254,28 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
134
254
|
regeneratingApiKey = undefined;
|
|
135
255
|
});
|
|
136
256
|
apiKey = await regeneratingApiKey;
|
|
137
|
-
|
|
257
|
+
await reply.send({ apiKey });
|
|
258
|
+
return;
|
|
138
259
|
}
|
|
139
260
|
}
|
|
140
261
|
if (apiRoute) {
|
|
141
|
-
|
|
142
|
-
|
|
262
|
+
const response = await handleSubscriptionAuthApi(options.auth, request, clientAbortSignal(request, reply));
|
|
263
|
+
if (response) {
|
|
264
|
+
const failure = await responseFailure(response);
|
|
265
|
+
if (failure)
|
|
266
|
+
requestErrors.set(request, failure);
|
|
267
|
+
await sendWebResponse(reply, response);
|
|
143
268
|
}
|
|
269
|
+
else
|
|
270
|
+
await reply.code(404).send({ error: "Not found" });
|
|
144
271
|
return;
|
|
145
272
|
}
|
|
146
273
|
const requested = url.pathname === "/" ? "index.html" : url.pathname;
|
|
147
274
|
const assetPath = resolve(ASSET_DIRECTORY, `.${requested}`);
|
|
148
275
|
const assetRelative = relative(ASSET_DIRECTORY, assetPath);
|
|
149
276
|
if (assetRelative.startsWith("..") || isAbsolute(assetRelative)) {
|
|
150
|
-
|
|
277
|
+
await reply.code(404).send({ error: "Not found" });
|
|
278
|
+
return;
|
|
151
279
|
}
|
|
152
280
|
let file = assetPath;
|
|
153
281
|
let body = await readFile(file).catch(() => null);
|
|
@@ -155,44 +283,37 @@ export async function createSubscriptionAuthDashboardServer(options) {
|
|
|
155
283
|
file = join(ASSET_DIRECTORY, "index.html");
|
|
156
284
|
body = await readFile(file);
|
|
157
285
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"cache-control"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
if (response.headersSent)
|
|
168
|
-
response.destroy();
|
|
169
|
-
else
|
|
170
|
-
sendJson(response, 400, { error: errorMessage(error) });
|
|
171
|
-
}
|
|
286
|
+
await reply
|
|
287
|
+
.type(CONTENT_TYPES[extname(file)] ?? "application/octet-stream")
|
|
288
|
+
.header("cache-control", relative(ASSET_DIRECTORY, file).startsWith("assets/")
|
|
289
|
+
? "public, max-age=31536000, immutable"
|
|
290
|
+
: "no-store")
|
|
291
|
+
.send(body);
|
|
292
|
+
},
|
|
172
293
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
server.off("error", reject);
|
|
177
|
-
resolve();
|
|
178
|
-
});
|
|
294
|
+
app.setErrorHandler(async (error, request, reply) => {
|
|
295
|
+
requestErrors.set(request, errorMessage(error));
|
|
296
|
+
await reply.code(400).send({ error: errorMessage(error) });
|
|
179
297
|
});
|
|
180
|
-
|
|
298
|
+
await app.listen({ port: options.port ?? 0, host });
|
|
299
|
+
const address = app.server.address();
|
|
181
300
|
if (!address || typeof address === "string") {
|
|
301
|
+
await app.close();
|
|
182
302
|
throw new Error("Unable to determine AI Subs dashboard port");
|
|
183
303
|
}
|
|
184
304
|
const url = `${origin}:${address.port}`;
|
|
185
305
|
return {
|
|
186
|
-
server,
|
|
306
|
+
server: app.server,
|
|
307
|
+
app,
|
|
187
308
|
get apiKey() {
|
|
188
309
|
return apiKey;
|
|
189
310
|
},
|
|
190
311
|
url,
|
|
191
312
|
bootstrapUrl: url,
|
|
192
|
-
close: () => {
|
|
313
|
+
close: async () => {
|
|
193
314
|
for (const stream of logStreams)
|
|
194
315
|
stream.end();
|
|
195
|
-
|
|
316
|
+
await app.close();
|
|
196
317
|
},
|
|
197
318
|
};
|
|
198
319
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
|
|
2
|
+
import type { Server } from "node:http";
|
|
2
3
|
import type { SubscriptionAuth } from "./auth.js";
|
|
3
|
-
export declare function readJsonBody(request: IncomingMessage): Promise<unknown>;
|
|
4
|
-
export declare function readProxyBody(request: IncomingMessage, maxBytes?: number): Promise<Buffer>;
|
|
5
|
-
export declare function sendJson(response: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void;
|
|
6
4
|
export declare function routeSegments(pathname: string): string[];
|
|
7
|
-
export declare function
|
|
5
|
+
export declare function sendWebResponse(reply: FastifyReply, upstream: Response): Promise<void>;
|
|
6
|
+
export declare function handleSubscriptionAuthApi(auth: SubscriptionAuth, request: FastifyRequest, signal?: AbortSignal): Promise<Response | null>;
|
|
7
|
+
/** Abort upstream work only when the client disconnects before the response finishes. */
|
|
8
|
+
export declare function clientAbortSignal(request: FastifyRequest, reply: FastifyReply): AbortSignal;
|
|
8
9
|
export interface SubscriptionAuthServerOptions {
|
|
9
10
|
auth: SubscriptionAuth;
|
|
10
11
|
apiKey: string;
|
|
11
12
|
host?: string;
|
|
12
13
|
port?: number;
|
|
13
|
-
/** Maximum
|
|
14
|
+
/** Maximum request body accepted by the compatibility proxy. Defaults to 10 MiB. */
|
|
14
15
|
maxProxyBodyBytes?: number;
|
|
15
16
|
}
|
|
16
17
|
export interface SubscriptionAuthServer {
|
|
17
18
|
server: Server;
|
|
19
|
+
app: FastifyInstance;
|
|
18
20
|
apiKey: string;
|
|
19
21
|
url: string;
|
|
20
22
|
close(): Promise<void>;
|
|
21
23
|
}
|
|
24
|
+
export declare function createApiApp(options: SubscriptionAuthServerOptions): FastifyInstance;
|
|
22
25
|
export declare function createSubscriptionAuthServer(options: SubscriptionAuthServerOptions): Promise<SubscriptionAuthServer>;
|