@corsair-dev/studio 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 +191 -0
- package/dist/server/index.js +1339 -0
- package/dist/src/server/dev-entry.d.ts +2 -0
- package/dist/src/server/dev-entry.d.ts.map +1 -0
- package/dist/src/server/handlers/auth.d.ts +4 -0
- package/dist/src/server/handlers/auth.d.ts.map +1 -0
- package/dist/src/server/handlers/credentials-internal.d.ts +21 -0
- package/dist/src/server/handlers/credentials-internal.d.ts.map +1 -0
- package/dist/src/server/handlers/credentials.d.ts +4 -0
- package/dist/src/server/handlers/credentials.d.ts.map +1 -0
- package/dist/src/server/handlers/db.d.ts +6 -0
- package/dist/src/server/handlers/db.d.ts.map +1 -0
- package/dist/src/server/handlers/operations.d.ts +6 -0
- package/dist/src/server/handlers/operations.d.ts.map +1 -0
- package/dist/src/server/handlers/plugin-setup.d.ts +3 -0
- package/dist/src/server/handlers/plugin-setup.d.ts.map +1 -0
- package/dist/src/server/handlers/plugins.d.ts +3 -0
- package/dist/src/server/handlers/plugins.d.ts.map +1 -0
- package/dist/src/server/handlers/status.d.ts +3 -0
- package/dist/src/server/handlers/status.d.ts.map +1 -0
- package/dist/src/server/handlers/tenants.d.ts +4 -0
- package/dist/src/server/handlers/tenants.d.ts.map +1 -0
- package/dist/src/server/index.d.ts +22 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/load.d.ts +5 -0
- package/dist/src/server/load.d.ts.map +1 -0
- package/dist/src/server/router.d.ts +5 -0
- package/dist/src/server/router.d.ts.map +1 -0
- package/dist/src/server/serve-static.d.ts +3 -0
- package/dist/src/server/serve-static.d.ts.map +1 -0
- package/dist/src/server/types.d.ts +21 -0
- package/dist/src/server/types.d.ts.map +1 -0
- package/dist/web/assets/index-BdMocr9R.js +52 -0
- package/dist/web/assets/index-BdMocr9R.js.map +1 -0
- package/dist/web/assets/index-D8r6TgYt.css +1 -0
- package/dist/web/index.html +14 -0
- package/package.json +45 -0
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
// src/server/index.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import http2 from "http";
|
|
4
|
+
import { findCorsairConfigPath } from "@corsair-dev/cli";
|
|
5
|
+
|
|
6
|
+
// src/server/load.ts
|
|
7
|
+
import { getCorsairInstance } from "@corsair-dev/cli";
|
|
8
|
+
import { CORSAIR_INTERNAL } from "corsair/core";
|
|
9
|
+
var cache = /* @__PURE__ */ new Map();
|
|
10
|
+
function loadCorsair(cwd, options = {}) {
|
|
11
|
+
if (options.force) {
|
|
12
|
+
cache.delete(cwd);
|
|
13
|
+
}
|
|
14
|
+
const cached = cache.get(cwd);
|
|
15
|
+
if (cached) return cached;
|
|
16
|
+
const pending = (async () => {
|
|
17
|
+
const instance = await getCorsairInstance({
|
|
18
|
+
cwd,
|
|
19
|
+
shouldThrowOnError: true
|
|
20
|
+
});
|
|
21
|
+
const internal = instance[CORSAIR_INTERNAL];
|
|
22
|
+
if (!internal) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Could not read internal config from Corsair instance. Upgrade the `corsair` package to the latest version."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const handle = {
|
|
28
|
+
instance,
|
|
29
|
+
internal,
|
|
30
|
+
resolveClient(tenantId) {
|
|
31
|
+
const obj = instance;
|
|
32
|
+
if (typeof obj.withTenant === "function") {
|
|
33
|
+
const tid = tenantId ?? "default";
|
|
34
|
+
return obj.withTenant(
|
|
35
|
+
tid
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return obj;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return handle;
|
|
42
|
+
})();
|
|
43
|
+
cache.set(cwd, pending);
|
|
44
|
+
pending.catch(() => cache.delete(cwd));
|
|
45
|
+
return pending;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/server/handlers/auth.ts
|
|
49
|
+
import * as http from "http";
|
|
50
|
+
import * as https from "https";
|
|
51
|
+
import * as net from "net";
|
|
52
|
+
import * as querystring from "querystring";
|
|
53
|
+
import {
|
|
54
|
+
createAccountKeyManager,
|
|
55
|
+
createIntegrationKeyManager
|
|
56
|
+
} from "corsair/core";
|
|
57
|
+
function getAuthType(plugin) {
|
|
58
|
+
return plugin.options?.authType;
|
|
59
|
+
}
|
|
60
|
+
function getOAuthConfig(plugin) {
|
|
61
|
+
return plugin.oauthConfig ?? null;
|
|
62
|
+
}
|
|
63
|
+
function getCustomIntegrationFields(plugin, authType) {
|
|
64
|
+
const authConfig = plugin.authConfig;
|
|
65
|
+
return authConfig?.[authType]?.integration ?? [];
|
|
66
|
+
}
|
|
67
|
+
function getCustomAccountFields(plugin, authType) {
|
|
68
|
+
const authConfig = plugin.authConfig;
|
|
69
|
+
return authConfig?.[authType]?.account ?? [];
|
|
70
|
+
}
|
|
71
|
+
function findFreePort() {
|
|
72
|
+
return new Promise((resolve2, reject) => {
|
|
73
|
+
const server = net.createServer();
|
|
74
|
+
server.listen(0, "127.0.0.1", () => {
|
|
75
|
+
const address = server.address();
|
|
76
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
77
|
+
server.close((err) => {
|
|
78
|
+
if (err) reject(err);
|
|
79
|
+
else resolve2(port);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
server.on("error", reject);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
var pendingListeners = /* @__PURE__ */ new Map();
|
|
86
|
+
function startCodeListener(port) {
|
|
87
|
+
let resolveCode;
|
|
88
|
+
let rejectCode;
|
|
89
|
+
const code = new Promise((resolve2, reject) => {
|
|
90
|
+
resolveCode = resolve2;
|
|
91
|
+
rejectCode = reject;
|
|
92
|
+
});
|
|
93
|
+
const server = http.createServer((req, res) => {
|
|
94
|
+
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
95
|
+
const receivedCode = reqUrl.searchParams.get("code");
|
|
96
|
+
const error = reqUrl.searchParams.get("error");
|
|
97
|
+
if (error) {
|
|
98
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
99
|
+
res.end(
|
|
100
|
+
`<html><body style="font-family:system-ui;padding:2rem;"><h2>Authorization failed</h2><p>${error}</p><p>You can close this tab.</p></body></html>`
|
|
101
|
+
);
|
|
102
|
+
server.close();
|
|
103
|
+
rejectCode(new Error(`OAuth error: ${error}`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (receivedCode) {
|
|
107
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
108
|
+
res.end(
|
|
109
|
+
`<html><body style="font-family:system-ui;padding:2rem;"><h2>Authorization successful</h2><p>You can close this tab and return to Corsair Studio.</p></body></html>`
|
|
110
|
+
);
|
|
111
|
+
server.close();
|
|
112
|
+
resolveCode(receivedCode);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
116
|
+
res.end("<html><body><h2>No code received.</h2></body></html>");
|
|
117
|
+
});
|
|
118
|
+
server.listen(port, "127.0.0.1");
|
|
119
|
+
server.on("error", rejectCode);
|
|
120
|
+
return { server, code };
|
|
121
|
+
}
|
|
122
|
+
function exchangeCodeForTokens(code, clientId, clientSecret, oauthConfig, redirectUri) {
|
|
123
|
+
const tokenUrl = new URL(oauthConfig.tokenUrl);
|
|
124
|
+
const useBasicAuth = oauthConfig.tokenAuthMethod === "basic";
|
|
125
|
+
return new Promise((resolve2, reject) => {
|
|
126
|
+
const params = {
|
|
127
|
+
code: code.trim(),
|
|
128
|
+
redirect_uri: redirectUri,
|
|
129
|
+
grant_type: "authorization_code"
|
|
130
|
+
};
|
|
131
|
+
if (!useBasicAuth) {
|
|
132
|
+
params.client_id = clientId;
|
|
133
|
+
params.client_secret = clientSecret;
|
|
134
|
+
}
|
|
135
|
+
const postData = querystring.stringify(params);
|
|
136
|
+
const headers = {
|
|
137
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
138
|
+
"Content-Length": Buffer.byteLength(postData).toString()
|
|
139
|
+
};
|
|
140
|
+
if (useBasicAuth) {
|
|
141
|
+
headers.Authorization = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
|
142
|
+
}
|
|
143
|
+
const req = https.request(
|
|
144
|
+
{
|
|
145
|
+
hostname: tokenUrl.hostname,
|
|
146
|
+
path: tokenUrl.pathname,
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers
|
|
149
|
+
},
|
|
150
|
+
(res) => {
|
|
151
|
+
let data = "";
|
|
152
|
+
res.on("data", (c) => {
|
|
153
|
+
data += c;
|
|
154
|
+
});
|
|
155
|
+
res.on("end", () => {
|
|
156
|
+
if (res.statusCode !== 200) {
|
|
157
|
+
reject(new Error(`Token exchange failed: ${data}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
resolve2(JSON.parse(data));
|
|
162
|
+
} catch {
|
|
163
|
+
reject(new Error(`Token exchange: invalid JSON response: ${data}`));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
req.on(
|
|
169
|
+
"error",
|
|
170
|
+
(e) => reject(new Error(`Token exchange request failed: ${e.message}`))
|
|
171
|
+
);
|
|
172
|
+
req.write(postData);
|
|
173
|
+
req.end();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
var startOAuth = async (ctx) => {
|
|
177
|
+
const body = await readJsonBody(ctx.req);
|
|
178
|
+
const pluginId = String(body.pluginId ?? "");
|
|
179
|
+
const tenantId = String(body.tenantId ?? "default");
|
|
180
|
+
if (!pluginId) throw new Error("Missing pluginId.");
|
|
181
|
+
const { internal } = await ctx.getCorsair();
|
|
182
|
+
const database = internal.database;
|
|
183
|
+
if (!database) throw new Error("No database configured.");
|
|
184
|
+
const plugin = internal.plugins.find((p) => p.id === pluginId);
|
|
185
|
+
if (!plugin) throw new Error(`Plugin '${pluginId}' not found.`);
|
|
186
|
+
const authType = getAuthType(plugin);
|
|
187
|
+
if (authType !== "oauth_2") {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Plugin '${pluginId}' uses auth type '${authType}', not OAuth. Set credentials via /api/credentials/set.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const oauthCfg = getOAuthConfig(plugin);
|
|
193
|
+
if (!oauthCfg) throw new Error(`No oauthConfig on plugin '${plugin.id}'.`);
|
|
194
|
+
const extraIntegration = getCustomIntegrationFields(plugin, "oauth_2");
|
|
195
|
+
const integrationKm = createIntegrationKeyManager({
|
|
196
|
+
authType: "oauth_2",
|
|
197
|
+
integrationName: plugin.id,
|
|
198
|
+
kek: internal.kek,
|
|
199
|
+
database,
|
|
200
|
+
extraIntegrationFields: extraIntegration
|
|
201
|
+
});
|
|
202
|
+
const clientId = await integrationKm.get_client_id();
|
|
203
|
+
const clientSecret = await integrationKm.get_client_secret();
|
|
204
|
+
if (!clientId || !clientSecret) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`client_id and/or client_secret not set for '${plugin.id}'. Set them in Credentials first.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
let redirectUri;
|
|
210
|
+
let localhostPort;
|
|
211
|
+
if (oauthCfg.requiresRegisteredRedirect) {
|
|
212
|
+
const stored = await integrationKm.get_redirect_url();
|
|
213
|
+
if (!stored) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`This provider requires a registered redirect_url. Set it in Credentials first.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
redirectUri = stored;
|
|
219
|
+
const m = redirectUri.match(/^https?:\/\/(?:localhost|127\.0\.0\.1):(\d+)/);
|
|
220
|
+
if (!m?.[1]) {
|
|
221
|
+
return {
|
|
222
|
+
status: "needs_code",
|
|
223
|
+
authUrl: buildAuthUrl(oauthCfg, clientId, redirectUri),
|
|
224
|
+
redirectUri,
|
|
225
|
+
note: "This redirect URI is not a localhost URL \u2014 complete the flow manually and paste the code back in."
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
localhostPort = parseInt(m[1], 10);
|
|
229
|
+
} else {
|
|
230
|
+
localhostPort = await findFreePort();
|
|
231
|
+
redirectUri = `http://localhost:${localhostPort}`;
|
|
232
|
+
}
|
|
233
|
+
const authUrl = buildAuthUrl(oauthCfg, clientId, redirectUri);
|
|
234
|
+
const key = `${pluginId}:${tenantId}`;
|
|
235
|
+
const existing = pendingListeners.get(key);
|
|
236
|
+
if (existing) {
|
|
237
|
+
existing.server.close();
|
|
238
|
+
pendingListeners.delete(key);
|
|
239
|
+
}
|
|
240
|
+
const { server, code } = startCodeListener(localhostPort);
|
|
241
|
+
pendingListeners.set(key, { redirectUri, codePromise: code, server });
|
|
242
|
+
code.then(async (receivedCode) => {
|
|
243
|
+
try {
|
|
244
|
+
const tokens = await exchangeCodeForTokens(
|
|
245
|
+
receivedCode,
|
|
246
|
+
clientId,
|
|
247
|
+
clientSecret,
|
|
248
|
+
oauthCfg,
|
|
249
|
+
redirectUri
|
|
250
|
+
);
|
|
251
|
+
if (!tokens.access_token) return;
|
|
252
|
+
const extraAccount = getCustomAccountFields(plugin, "oauth_2");
|
|
253
|
+
const accountKm = createAccountKeyManager({
|
|
254
|
+
authType: "oauth_2",
|
|
255
|
+
integrationName: plugin.id,
|
|
256
|
+
tenantId,
|
|
257
|
+
kek: internal.kek,
|
|
258
|
+
database,
|
|
259
|
+
extraAccountFields: extraAccount
|
|
260
|
+
});
|
|
261
|
+
await accountKm.set_access_token(tokens.access_token);
|
|
262
|
+
if (tokens.refresh_token)
|
|
263
|
+
await accountKm.set_refresh_token(tokens.refresh_token);
|
|
264
|
+
if (tokens.expires_in)
|
|
265
|
+
await accountKm.set_expires_at(
|
|
266
|
+
(Math.floor(Date.now() / 1e3) + tokens.expires_in).toString()
|
|
267
|
+
);
|
|
268
|
+
} finally {
|
|
269
|
+
pendingListeners.delete(key);
|
|
270
|
+
}
|
|
271
|
+
}).catch(() => {
|
|
272
|
+
pendingListeners.delete(key);
|
|
273
|
+
});
|
|
274
|
+
return { status: "pending_oauth", authUrl, redirectUri };
|
|
275
|
+
};
|
|
276
|
+
function buildAuthUrl(oauthCfg, clientId, redirectUri) {
|
|
277
|
+
const params = {
|
|
278
|
+
client_id: clientId,
|
|
279
|
+
redirect_uri: redirectUri,
|
|
280
|
+
response_type: "code",
|
|
281
|
+
scope: oauthCfg.scopes.join(" "),
|
|
282
|
+
...oauthCfg.authParams
|
|
283
|
+
};
|
|
284
|
+
return `${oauthCfg.authUrl}?${querystring.stringify(params)}`;
|
|
285
|
+
}
|
|
286
|
+
var exchangeOAuth = async (ctx) => {
|
|
287
|
+
const body = await readJsonBody(ctx.req);
|
|
288
|
+
const pluginId = String(body.pluginId ?? "");
|
|
289
|
+
const tenantId = String(body.tenantId ?? "default");
|
|
290
|
+
const code = String(body.code ?? "");
|
|
291
|
+
if (!pluginId) throw new Error("Missing pluginId.");
|
|
292
|
+
if (!code) throw new Error("Missing code.");
|
|
293
|
+
const { internal } = await ctx.getCorsair();
|
|
294
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
295
|
+
const plugin = internal.plugins.find((p) => p.id === pluginId);
|
|
296
|
+
if (!plugin) throw new Error(`Plugin '${pluginId}' not found.`);
|
|
297
|
+
const oauthCfg = getOAuthConfig(plugin);
|
|
298
|
+
if (!oauthCfg) throw new Error(`No oauthConfig on plugin '${plugin.id}'.`);
|
|
299
|
+
const extraIntegration = getCustomIntegrationFields(plugin, "oauth_2");
|
|
300
|
+
const integrationKm = createIntegrationKeyManager({
|
|
301
|
+
authType: "oauth_2",
|
|
302
|
+
integrationName: plugin.id,
|
|
303
|
+
kek: internal.kek,
|
|
304
|
+
database: internal.database,
|
|
305
|
+
extraIntegrationFields: extraIntegration
|
|
306
|
+
});
|
|
307
|
+
const clientId = await integrationKm.get_client_id();
|
|
308
|
+
const clientSecret = await integrationKm.get_client_secret();
|
|
309
|
+
const redirectUri = await integrationKm.get_redirect_url() ?? "";
|
|
310
|
+
if (!clientId || !clientSecret) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
"client_id/client_secret not set. Set them in Credentials first."
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const tokens = await exchangeCodeForTokens(
|
|
316
|
+
code,
|
|
317
|
+
clientId,
|
|
318
|
+
clientSecret,
|
|
319
|
+
oauthCfg,
|
|
320
|
+
redirectUri
|
|
321
|
+
);
|
|
322
|
+
if (!tokens.access_token) {
|
|
323
|
+
throw new Error("No access_token in response from provider.");
|
|
324
|
+
}
|
|
325
|
+
const extraAccount = getCustomAccountFields(plugin, "oauth_2");
|
|
326
|
+
const accountKm = createAccountKeyManager({
|
|
327
|
+
authType: "oauth_2",
|
|
328
|
+
integrationName: plugin.id,
|
|
329
|
+
tenantId,
|
|
330
|
+
kek: internal.kek,
|
|
331
|
+
database: internal.database,
|
|
332
|
+
extraAccountFields: extraAccount
|
|
333
|
+
});
|
|
334
|
+
await accountKm.set_access_token(tokens.access_token);
|
|
335
|
+
if (tokens.refresh_token)
|
|
336
|
+
await accountKm.set_refresh_token(tokens.refresh_token);
|
|
337
|
+
if (tokens.expires_in)
|
|
338
|
+
await accountKm.set_expires_at(
|
|
339
|
+
(Math.floor(Date.now() / 1e3) + tokens.expires_in).toString()
|
|
340
|
+
);
|
|
341
|
+
return { ok: true };
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// src/server/handlers/credentials-internal.ts
|
|
345
|
+
var BASE_FIELDS = {
|
|
346
|
+
oauth_2: {
|
|
347
|
+
integration: ["client_id", "client_secret", "redirect_url"],
|
|
348
|
+
account: ["access_token", "refresh_token", "expires_at"]
|
|
349
|
+
},
|
|
350
|
+
api_key: { integration: [], account: ["api_key"] },
|
|
351
|
+
bot_token: { integration: [], account: ["bot_token"] }
|
|
352
|
+
};
|
|
353
|
+
function getCustomFields(plugin, authType) {
|
|
354
|
+
const authConfig = plugin.authConfig;
|
|
355
|
+
const entry = authConfig?.[authType];
|
|
356
|
+
return {
|
|
357
|
+
integration: entry?.integration ?? [],
|
|
358
|
+
account: entry?.account ?? []
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function getAuthFieldStatus(opts) {
|
|
362
|
+
const {
|
|
363
|
+
authType,
|
|
364
|
+
extraIntegration,
|
|
365
|
+
extraAccount,
|
|
366
|
+
integrationNamespace,
|
|
367
|
+
accountNamespace,
|
|
368
|
+
includeIntegration = true,
|
|
369
|
+
includeAccount = true
|
|
370
|
+
} = opts;
|
|
371
|
+
if (!authType) return [];
|
|
372
|
+
const base = BASE_FIELDS[authType];
|
|
373
|
+
const integrationFields = Array.from(
|
|
374
|
+
/* @__PURE__ */ new Set([...base.integration, ...extraIntegration])
|
|
375
|
+
);
|
|
376
|
+
const accountFields = Array.from(/* @__PURE__ */ new Set([...base.account, ...extraAccount]));
|
|
377
|
+
const result = [];
|
|
378
|
+
if (includeIntegration && integrationFields.length > 0) {
|
|
379
|
+
for (const f of integrationFields) {
|
|
380
|
+
const value = await tryGet(integrationNamespace, f);
|
|
381
|
+
result.push({ name: f, level: "integration", set: !!value });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (includeAccount && accountFields.length > 0) {
|
|
385
|
+
for (const f of accountFields) {
|
|
386
|
+
const value = await tryGet(accountNamespace, f);
|
|
387
|
+
result.push({ name: f, level: "account", set: !!value });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
async function tryGet(km, field) {
|
|
393
|
+
const fn = km[`get_${field}`];
|
|
394
|
+
if (typeof fn !== "function") return null;
|
|
395
|
+
try {
|
|
396
|
+
return await fn();
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function maskValue(value) {
|
|
402
|
+
if (!value) return null;
|
|
403
|
+
return value.length <= 9 ? "***" : `${value.slice(0, 6)}...${value.slice(-3)}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/server/handlers/credentials.ts
|
|
407
|
+
var BASE = {
|
|
408
|
+
oauth_2: {
|
|
409
|
+
integration: ["client_id", "client_secret", "redirect_url"],
|
|
410
|
+
account: [
|
|
411
|
+
"access_token",
|
|
412
|
+
"refresh_token",
|
|
413
|
+
"expires_at",
|
|
414
|
+
"webhook_signature"
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
api_key: { integration: [], account: ["api_key", "webhook_signature"] },
|
|
418
|
+
bot_token: { integration: [], account: ["bot_token", "webhook_signature"] }
|
|
419
|
+
};
|
|
420
|
+
var getCredentials = async (ctx) => {
|
|
421
|
+
const body = await readJsonBody(ctx.req);
|
|
422
|
+
const pluginId = String(body.pluginId ?? "");
|
|
423
|
+
const tenantId = String(body.tenantId ?? "default");
|
|
424
|
+
const scope = body.scope === "main" ? "main" : "tenant";
|
|
425
|
+
const showRaw = body.showRaw === true;
|
|
426
|
+
const { internal, instance, resolveClient } = await ctx.getCorsair();
|
|
427
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
428
|
+
const plugin = internal.plugins.find((p) => p.id === pluginId);
|
|
429
|
+
if (!plugin) throw new Error(`Plugin '${pluginId}' not found.`);
|
|
430
|
+
const authType = plugin.options?.authType;
|
|
431
|
+
const state = {
|
|
432
|
+
authType: authType ?? null,
|
|
433
|
+
integration: {},
|
|
434
|
+
account: {}
|
|
435
|
+
};
|
|
436
|
+
if (!authType) return state;
|
|
437
|
+
const custom = getCustomFields(plugin, authType);
|
|
438
|
+
const base = BASE[authType];
|
|
439
|
+
const integrationFields = Array.from(
|
|
440
|
+
/* @__PURE__ */ new Set([...base.integration, ...custom.integration])
|
|
441
|
+
);
|
|
442
|
+
const accountFields = Array.from(
|
|
443
|
+
/* @__PURE__ */ new Set([...base.account, ...custom.account])
|
|
444
|
+
);
|
|
445
|
+
const rootKeys = instance.keys ?? null;
|
|
446
|
+
const integrationNamespace = rootKeys?.[pluginId] ?? null;
|
|
447
|
+
const client = resolveClient(tenantId);
|
|
448
|
+
const pluginNamespace = client[pluginId] ?? null;
|
|
449
|
+
const accountNamespace = scope === "tenant" ? pluginNamespace?.keys ?? null : null;
|
|
450
|
+
if (integrationFields.length > 0) {
|
|
451
|
+
for (const f of integrationFields) {
|
|
452
|
+
const fn = integrationNamespace?.[`get_${f}`];
|
|
453
|
+
const v = typeof fn === "function" ? await fn() : null;
|
|
454
|
+
state.integration[f] = showRaw ? v : maskValue(v);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (scope === "tenant" && accountFields.length > 0) {
|
|
458
|
+
for (const f of accountFields) {
|
|
459
|
+
const fn = accountNamespace?.[`get_${f}`];
|
|
460
|
+
const v = typeof fn === "function" ? await fn() : null;
|
|
461
|
+
state.account[f] = showRaw ? v : maskValue(v);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return state;
|
|
465
|
+
};
|
|
466
|
+
var setCredentials = async (ctx) => {
|
|
467
|
+
const body = await readJsonBody(ctx.req);
|
|
468
|
+
const pluginId = String(body.pluginId ?? "");
|
|
469
|
+
const tenantId = String(body.tenantId ?? "default");
|
|
470
|
+
const scope = body.scope === "main" ? "main" : "tenant";
|
|
471
|
+
const fields = body.fields ?? {};
|
|
472
|
+
const { internal, instance, resolveClient } = await ctx.getCorsair();
|
|
473
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
474
|
+
const plugin = internal.plugins.find((p) => p.id === pluginId);
|
|
475
|
+
if (!plugin) throw new Error(`Plugin '${pluginId}' not found.`);
|
|
476
|
+
const authType = plugin.options?.authType;
|
|
477
|
+
if (!authType) {
|
|
478
|
+
throw new Error(`Plugin '${pluginId}' does not require credentials.`);
|
|
479
|
+
}
|
|
480
|
+
const custom = getCustomFields(plugin, authType);
|
|
481
|
+
const base = BASE[authType];
|
|
482
|
+
const integrationFields = /* @__PURE__ */ new Set([
|
|
483
|
+
...base.integration,
|
|
484
|
+
...custom.integration
|
|
485
|
+
]);
|
|
486
|
+
const accountFields = /* @__PURE__ */ new Set([...base.account, ...custom.account]);
|
|
487
|
+
const rootKeys = instance.keys ?? null;
|
|
488
|
+
const integrationNamespace = rootKeys?.[pluginId] ?? null;
|
|
489
|
+
const client = resolveClient(tenantId);
|
|
490
|
+
const pluginNamespace = client[pluginId] ?? null;
|
|
491
|
+
const accountNamespace = scope === "tenant" ? pluginNamespace?.keys ?? null : null;
|
|
492
|
+
const updated = [];
|
|
493
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
494
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
495
|
+
if (integrationFields.has(field)) {
|
|
496
|
+
const setter = integrationNamespace?.[`set_${field}`];
|
|
497
|
+
if (typeof setter === "function") {
|
|
498
|
+
await setter(value);
|
|
499
|
+
updated.push(`integration:${field}`);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (scope === "tenant" && accountFields.has(field)) {
|
|
504
|
+
const setter = accountNamespace?.[`set_${field}`];
|
|
505
|
+
if (typeof setter === "function") {
|
|
506
|
+
await setter(value);
|
|
507
|
+
updated.push(`account:${field}`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
throw new Error(`Unknown field '${field}' for plugin '${pluginId}'.`);
|
|
512
|
+
}
|
|
513
|
+
return { ok: true, updated };
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/server/handlers/db.ts
|
|
517
|
+
var CORE_TABLES = [
|
|
518
|
+
"corsair_integrations",
|
|
519
|
+
"corsair_accounts",
|
|
520
|
+
"corsair_entities",
|
|
521
|
+
"corsair_events",
|
|
522
|
+
"corsair_permissions"
|
|
523
|
+
];
|
|
524
|
+
function getKyselyDb(database) {
|
|
525
|
+
if (!database) return null;
|
|
526
|
+
const obj = database;
|
|
527
|
+
if (obj.db && typeof obj.db === "object" && "selectFrom" in obj.db) {
|
|
528
|
+
return obj.db;
|
|
529
|
+
}
|
|
530
|
+
if ("selectFrom" in obj) return obj;
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
var listDbTables = async (ctx) => {
|
|
534
|
+
const { internal } = await ctx.getCorsair();
|
|
535
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
536
|
+
const db = getKyselyDb(internal.database);
|
|
537
|
+
if (!db) throw new Error("Could not access kysely db handle.");
|
|
538
|
+
const existing = await db.introspection.getTables();
|
|
539
|
+
const existingNames = new Set(existing.map((t) => t.name));
|
|
540
|
+
return {
|
|
541
|
+
core: CORE_TABLES.filter((t) => existingNames.has(t)),
|
|
542
|
+
missing: CORE_TABLES.filter((t) => !existingNames.has(t)),
|
|
543
|
+
all: existing.map((t) => t.name).sort()
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
var listDbRows = async (ctx) => {
|
|
547
|
+
const body = await readJsonBody(ctx.req);
|
|
548
|
+
const table = String(body.table ?? "");
|
|
549
|
+
const limit = Math.min(Math.max(Number(body.limit ?? 50), 1), 500);
|
|
550
|
+
const offset = Math.max(Number(body.offset ?? 0), 0);
|
|
551
|
+
if (!table) throw new Error("Missing table.");
|
|
552
|
+
const { internal } = await ctx.getCorsair();
|
|
553
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
554
|
+
const db = getKyselyDb(internal.database);
|
|
555
|
+
if (!db) throw new Error("Could not access kysely db handle.");
|
|
556
|
+
let rows;
|
|
557
|
+
try {
|
|
558
|
+
const q = db.selectFrom(table).selectAll().limit(limit).offset(offset);
|
|
559
|
+
rows = await q.orderBy("created_at", "desc").execute();
|
|
560
|
+
} catch {
|
|
561
|
+
rows = await db.selectFrom(table).selectAll().limit(limit).offset(offset).execute();
|
|
562
|
+
}
|
|
563
|
+
const safeRows = rows.map(redactSensitive);
|
|
564
|
+
return { rows: safeRows, limit, offset };
|
|
565
|
+
};
|
|
566
|
+
var queryEntityData = async (ctx) => {
|
|
567
|
+
const body = await readJsonBody(ctx.req);
|
|
568
|
+
const tenant = String(body.tenant ?? "").trim();
|
|
569
|
+
const integration = String(body.integration ?? "").trim();
|
|
570
|
+
const entity = String(body.entity ?? "").trim();
|
|
571
|
+
const search = String(body.search ?? "").trim();
|
|
572
|
+
const limit = Math.min(Math.max(Number(body.limit ?? 30), 1), 200);
|
|
573
|
+
const offset = Math.max(Number(body.offset ?? 0), 0);
|
|
574
|
+
const rawSortField = String(body.sortField ?? "updated_at");
|
|
575
|
+
const rawSortDirection = String(body.sortDirection ?? "desc");
|
|
576
|
+
const sortField = rawSortField === "created_at" || rawSortField === "updated_at" ? rawSortField : "updated_at";
|
|
577
|
+
const sortDirection = rawSortDirection === "asc" || rawSortDirection === "desc" ? rawSortDirection : "desc";
|
|
578
|
+
if (!tenant) throw new Error("Missing tenant.");
|
|
579
|
+
if (!integration) throw new Error("Missing integration.");
|
|
580
|
+
if (!entity) throw new Error("Missing entity.");
|
|
581
|
+
const { internal } = await ctx.getCorsair();
|
|
582
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
583
|
+
const db = getKyselyDb(internal.database);
|
|
584
|
+
if (!db) throw new Error("Could not access kysely db handle.");
|
|
585
|
+
const base = db.selectFrom("corsair_entities as e").innerJoin("corsair_accounts as a", "a.id", "e.account_id").innerJoin("corsair_integrations as i", "i.id", "a.integration_id").where("a.tenant_id", "=", tenant).where("i.name", "=", integration).where("e.entity_type", "=", entity);
|
|
586
|
+
let rowsRaw = [];
|
|
587
|
+
let hasMore = false;
|
|
588
|
+
let total = 0;
|
|
589
|
+
if (!search) {
|
|
590
|
+
const page = await base.select(["e.id as id", "e.entity_id as entity_id", "e.data as data"]).orderBy(`e.${sortField}`, sortDirection).limit(limit + 1).offset(offset).execute();
|
|
591
|
+
hasMore = page.length > limit;
|
|
592
|
+
rowsRaw = hasMore ? page.slice(0, limit) : page;
|
|
593
|
+
total = hasMore ? offset + limit + 1 : offset + rowsRaw.length;
|
|
594
|
+
} else {
|
|
595
|
+
const matched = [];
|
|
596
|
+
const batchSize = 200;
|
|
597
|
+
let scanOffset = 0;
|
|
598
|
+
let matchedBeforePage = 0;
|
|
599
|
+
while (true) {
|
|
600
|
+
const batch = await base.select(["e.id as id", "e.entity_id as entity_id", "e.data as data"]).orderBy(`e.${sortField}`, sortDirection).limit(batchSize).offset(scanOffset).execute();
|
|
601
|
+
if (batch.length === 0) {
|
|
602
|
+
hasMore = false;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
for (const row of batch) {
|
|
606
|
+
if (!dataMatchesSearch(row.data, search)) continue;
|
|
607
|
+
if (matchedBeforePage < offset) {
|
|
608
|
+
matchedBeforePage += 1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
matched.push(row);
|
|
612
|
+
if (matched.length > limit) {
|
|
613
|
+
hasMore = true;
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (hasMore) break;
|
|
618
|
+
scanOffset += batch.length;
|
|
619
|
+
if (batch.length < batchSize) break;
|
|
620
|
+
}
|
|
621
|
+
rowsRaw = matched.slice(0, limit);
|
|
622
|
+
total = matchedBeforePage + rowsRaw.length + (hasMore ? 1 : 0);
|
|
623
|
+
}
|
|
624
|
+
const rows = rowsRaw.map((row) => ({
|
|
625
|
+
id: row.id,
|
|
626
|
+
entity_id: row.entity_id,
|
|
627
|
+
data: row.data
|
|
628
|
+
})).map(redactSensitive);
|
|
629
|
+
return {
|
|
630
|
+
rows,
|
|
631
|
+
limit,
|
|
632
|
+
offset,
|
|
633
|
+
total,
|
|
634
|
+
hasMore
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
function redactSensitive(row) {
|
|
638
|
+
const copy = { ...row };
|
|
639
|
+
if ("dek" in copy && typeof copy.dek === "string" && copy.dek.length > 0) {
|
|
640
|
+
copy.dek = "***";
|
|
641
|
+
}
|
|
642
|
+
const config = copy.config;
|
|
643
|
+
if (config && typeof config === "object" && !Array.isArray(config)) {
|
|
644
|
+
const c = config;
|
|
645
|
+
const masked = {};
|
|
646
|
+
for (const [k, v] of Object.entries(c)) {
|
|
647
|
+
if (typeof v === "string" && v.length > 12) {
|
|
648
|
+
masked[k] = `${v.slice(0, 4)}\u2026${v.slice(-3)} (${v.length})`;
|
|
649
|
+
} else if (typeof v === "string") {
|
|
650
|
+
masked[k] = "***";
|
|
651
|
+
} else {
|
|
652
|
+
masked[k] = v;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
copy.config = masked;
|
|
656
|
+
}
|
|
657
|
+
return copy;
|
|
658
|
+
}
|
|
659
|
+
function dataMatchesSearch(data, rawSearch) {
|
|
660
|
+
const search = rawSearch.toLowerCase();
|
|
661
|
+
if (!search) return true;
|
|
662
|
+
const values = collectPrimitiveValues(data);
|
|
663
|
+
return values.some((value) => value.toLowerCase().includes(search));
|
|
664
|
+
}
|
|
665
|
+
function collectPrimitiveValues(value) {
|
|
666
|
+
if (value === null || value === void 0) return [];
|
|
667
|
+
if (typeof value === "string") return [value];
|
|
668
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
669
|
+
return [String(value)];
|
|
670
|
+
}
|
|
671
|
+
if (value instanceof Date) return [value.toISOString()];
|
|
672
|
+
if (Array.isArray(value)) {
|
|
673
|
+
return value.flatMap((item) => collectPrimitiveValues(item));
|
|
674
|
+
}
|
|
675
|
+
if (typeof value === "object") {
|
|
676
|
+
return Object.values(value).flatMap(
|
|
677
|
+
(v) => collectPrimitiveValues(v)
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
var listPermissions = async (ctx) => {
|
|
683
|
+
const { internal } = await ctx.getCorsair();
|
|
684
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
685
|
+
const db = getKyselyDb(internal.database);
|
|
686
|
+
if (!db) throw new Error("Could not access kysely db handle.");
|
|
687
|
+
const limit = Math.min(
|
|
688
|
+
Math.max(Number(ctx.url.searchParams.get("limit") ?? 100), 1),
|
|
689
|
+
500
|
|
690
|
+
);
|
|
691
|
+
try {
|
|
692
|
+
const rows = await db.selectFrom("corsair_permissions").selectAll().limit(limit).offset(0).orderBy("created_at", "desc").execute();
|
|
693
|
+
return { rows };
|
|
694
|
+
} catch (err) {
|
|
695
|
+
return { rows: [], note: err.message };
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
// src/server/handlers/operations.ts
|
|
700
|
+
function navigateToEndpoint(client, path) {
|
|
701
|
+
const parts = path.split(".");
|
|
702
|
+
let current = client;
|
|
703
|
+
for (const part of parts) {
|
|
704
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
705
|
+
current = current[part];
|
|
706
|
+
}
|
|
707
|
+
return typeof current === "function" ? current : void 0;
|
|
708
|
+
}
|
|
709
|
+
var listOperations = async (ctx) => {
|
|
710
|
+
const body = await readJsonBody(ctx.req);
|
|
711
|
+
const plugin = body.plugin ? String(body.plugin) : void 0;
|
|
712
|
+
const type = body.type ? String(body.type) : void 0;
|
|
713
|
+
const { instance } = await ctx.getCorsair();
|
|
714
|
+
const corsair = instance;
|
|
715
|
+
if (typeof corsair.list_operations !== "function") {
|
|
716
|
+
throw new Error("list_operations not available on this Corsair instance.");
|
|
717
|
+
}
|
|
718
|
+
const opts = {};
|
|
719
|
+
if (plugin) opts.plugin = plugin;
|
|
720
|
+
if (type === "api" || type === "webhooks" || type === "db") opts.type = type;
|
|
721
|
+
return corsair.list_operations(opts);
|
|
722
|
+
};
|
|
723
|
+
var schemaForOperation = async (ctx) => {
|
|
724
|
+
const body = await readJsonBody(ctx.req);
|
|
725
|
+
const path = String(body.path ?? "");
|
|
726
|
+
if (!path) throw new Error("Missing path.");
|
|
727
|
+
const { instance } = await ctx.getCorsair();
|
|
728
|
+
const corsair = instance;
|
|
729
|
+
if (typeof corsair.get_schema !== "function") {
|
|
730
|
+
throw new Error("get_schema not available on this Corsair instance.");
|
|
731
|
+
}
|
|
732
|
+
const schema = corsair.get_schema(path);
|
|
733
|
+
return { schema };
|
|
734
|
+
};
|
|
735
|
+
var runOperation = async (ctx) => {
|
|
736
|
+
const body = await readJsonBody(ctx.req);
|
|
737
|
+
const path = String(body.path ?? "");
|
|
738
|
+
const tenant = body.tenant ? String(body.tenant) : void 0;
|
|
739
|
+
const input = body.input ?? {};
|
|
740
|
+
if (!path) throw new Error("Missing path.");
|
|
741
|
+
const handle = await ctx.getCorsair();
|
|
742
|
+
const client = handle.resolveClient(tenant);
|
|
743
|
+
const fn = navigateToEndpoint(client, path);
|
|
744
|
+
if (!fn) throw new Error(`Could not find endpoint '${path}'.`);
|
|
745
|
+
const started = Date.now();
|
|
746
|
+
try {
|
|
747
|
+
const result = await fn(input);
|
|
748
|
+
return {
|
|
749
|
+
ok: true,
|
|
750
|
+
durationMs: Date.now() - started,
|
|
751
|
+
result
|
|
752
|
+
};
|
|
753
|
+
} catch (err) {
|
|
754
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
755
|
+
return {
|
|
756
|
+
ok: false,
|
|
757
|
+
durationMs: Date.now() - started,
|
|
758
|
+
error: message.slice(0, 4e3)
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
var runScript = async (ctx) => {
|
|
763
|
+
const body = await readJsonBody(ctx.req);
|
|
764
|
+
const code = String(body.code ?? "");
|
|
765
|
+
const tenant = body.tenant ? String(body.tenant) : void 0;
|
|
766
|
+
if (!code) throw new Error("Missing code.");
|
|
767
|
+
const handle = await ctx.getCorsair();
|
|
768
|
+
const client = handle.resolveClient(tenant);
|
|
769
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
770
|
+
}).constructor;
|
|
771
|
+
const fn = new AsyncFunction("corsair", code);
|
|
772
|
+
const started = Date.now();
|
|
773
|
+
try {
|
|
774
|
+
const result = await fn(client);
|
|
775
|
+
return {
|
|
776
|
+
ok: true,
|
|
777
|
+
durationMs: Date.now() - started,
|
|
778
|
+
result: result ?? null
|
|
779
|
+
};
|
|
780
|
+
} catch (err) {
|
|
781
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
durationMs: Date.now() - started,
|
|
785
|
+
error: message.slice(0, 4e3)
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// src/server/handlers/plugin-setup.ts
|
|
791
|
+
import { randomUUID } from "crypto";
|
|
792
|
+
function asDb(internalDatabase) {
|
|
793
|
+
const wrapped = internalDatabase;
|
|
794
|
+
const candidate = wrapped.db ?? internalDatabase;
|
|
795
|
+
if (!candidate || typeof candidate !== "object" || !("selectFrom" in candidate && typeof candidate.selectFrom === "function") || !("insertInto" in candidate && typeof candidate.insertInto === "function")) {
|
|
796
|
+
throw new Error("Could not access kysely db handle.");
|
|
797
|
+
}
|
|
798
|
+
return candidate;
|
|
799
|
+
}
|
|
800
|
+
function hasAuthConfig(internal, pluginId) {
|
|
801
|
+
const plugin = internal.plugins.find((p) => p.id === pluginId);
|
|
802
|
+
if (!plugin) return false;
|
|
803
|
+
const options = plugin.options;
|
|
804
|
+
return typeof options?.authType === "string";
|
|
805
|
+
}
|
|
806
|
+
var setupPlugin = async (ctx) => {
|
|
807
|
+
const body = await readJsonBody(ctx.req);
|
|
808
|
+
const pluginId = String(body.pluginId ?? "").trim();
|
|
809
|
+
const tenantId = String(body.tenantId ?? "default").trim() || "default";
|
|
810
|
+
if (!pluginId) throw new Error("pluginId is required.");
|
|
811
|
+
const { internal, instance, resolveClient } = await ctx.getCorsair();
|
|
812
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
813
|
+
if (!hasAuthConfig(internal, pluginId)) {
|
|
814
|
+
throw new Error(`Plugin '${pluginId}' has no credential setup.`);
|
|
815
|
+
}
|
|
816
|
+
const db = asDb(internal.database);
|
|
817
|
+
const now = /* @__PURE__ */ new Date();
|
|
818
|
+
let integration = await db.selectFrom("corsair_integrations").selectAll().where("name", "=", pluginId).executeTakeFirst();
|
|
819
|
+
if (!integration) {
|
|
820
|
+
const id = randomUUID();
|
|
821
|
+
await db.insertInto("corsair_integrations").values({
|
|
822
|
+
id,
|
|
823
|
+
name: pluginId,
|
|
824
|
+
config: {},
|
|
825
|
+
created_at: now,
|
|
826
|
+
updated_at: now
|
|
827
|
+
}).execute();
|
|
828
|
+
integration = await db.selectFrom("corsair_integrations").selectAll().where("id", "=", id).executeTakeFirst();
|
|
829
|
+
}
|
|
830
|
+
if (!integration) {
|
|
831
|
+
throw new Error(`Failed to create integration for '${pluginId}'.`);
|
|
832
|
+
}
|
|
833
|
+
const integrationId = integration.id;
|
|
834
|
+
if (typeof integrationId !== "string") {
|
|
835
|
+
throw new Error(`Invalid integration row for '${pluginId}'.`);
|
|
836
|
+
}
|
|
837
|
+
let account = await db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
|
|
838
|
+
if (!account) {
|
|
839
|
+
await db.insertInto("corsair_accounts").values({
|
|
840
|
+
id: randomUUID(),
|
|
841
|
+
tenant_id: tenantId,
|
|
842
|
+
integration_id: integrationId,
|
|
843
|
+
config: {},
|
|
844
|
+
created_at: now,
|
|
845
|
+
updated_at: now
|
|
846
|
+
}).execute();
|
|
847
|
+
account = await db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
|
|
848
|
+
}
|
|
849
|
+
const rootKeys = instance.keys ?? null;
|
|
850
|
+
const integrationNamespace = rootKeys?.[pluginId] ?? null;
|
|
851
|
+
if (!integration.dek) {
|
|
852
|
+
const issueIntegrationDek = integrationNamespace?.issue_new_dek;
|
|
853
|
+
if (typeof issueIntegrationDek === "function") {
|
|
854
|
+
await issueIntegrationDek();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (account && !account.dek) {
|
|
858
|
+
const tenantClient = resolveClient(tenantId);
|
|
859
|
+
const pluginNamespace = tenantClient[pluginId] ?? null;
|
|
860
|
+
const accountKeys = pluginNamespace?.keys ?? null;
|
|
861
|
+
const issueAccountDek = accountKeys?.issue_new_dek;
|
|
862
|
+
if (typeof issueAccountDek === "function") {
|
|
863
|
+
await issueAccountDek();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { ok: true };
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// src/server/handlers/plugins.ts
|
|
870
|
+
function getAuthType2(plugin) {
|
|
871
|
+
return plugin.options?.authType;
|
|
872
|
+
}
|
|
873
|
+
function getOAuthConfig2(plugin) {
|
|
874
|
+
return plugin.oauthConfig ?? null;
|
|
875
|
+
}
|
|
876
|
+
function getCustomFields2(plugin, authType) {
|
|
877
|
+
const authConfig = plugin.authConfig;
|
|
878
|
+
const entry = authConfig?.[authType];
|
|
879
|
+
return {
|
|
880
|
+
integration: entry?.integration ?? [],
|
|
881
|
+
account: entry?.account ?? []
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
var listPlugins = async (ctx) => {
|
|
885
|
+
const { internal, instance, resolveClient } = await ctx.getCorsair();
|
|
886
|
+
const tenantId = ctx.url.searchParams.get("tenant") ?? "default";
|
|
887
|
+
const scope = ctx.url.searchParams.get("scope") === "main" ? "main" : "tenant";
|
|
888
|
+
const client = resolveClient(tenantId);
|
|
889
|
+
const rootKeys = instance.keys ?? null;
|
|
890
|
+
const result = [];
|
|
891
|
+
for (const plugin of internal.plugins) {
|
|
892
|
+
const authType = getAuthType2(plugin);
|
|
893
|
+
const oauth = getOAuthConfig2(plugin);
|
|
894
|
+
const custom = authType ? getCustomFields2(plugin, authType) : { integration: [], account: [] };
|
|
895
|
+
const integrationNamespace = rootKeys?.[plugin.id] ?? null;
|
|
896
|
+
const pluginNamespace = client[plugin.id] ?? null;
|
|
897
|
+
const accountNamespace = pluginNamespace?.keys ?? null;
|
|
898
|
+
const status = await getAuthFieldStatus({
|
|
899
|
+
authType,
|
|
900
|
+
extraIntegration: custom.integration,
|
|
901
|
+
extraAccount: custom.account,
|
|
902
|
+
integrationNamespace,
|
|
903
|
+
accountNamespace,
|
|
904
|
+
includeIntegration: true,
|
|
905
|
+
includeAccount: scope !== "main"
|
|
906
|
+
});
|
|
907
|
+
result.push({
|
|
908
|
+
id: plugin.id,
|
|
909
|
+
authType: authType ?? "none",
|
|
910
|
+
authed: status.every((s) => s.set),
|
|
911
|
+
requiredFields: status,
|
|
912
|
+
oauth: oauth ? {
|
|
913
|
+
available: true,
|
|
914
|
+
scopes: oauth.scopes,
|
|
915
|
+
providerName: oauth.providerName ?? null
|
|
916
|
+
} : null
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
return result;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/server/handlers/status.ts
|
|
923
|
+
var getStatus = async (ctx) => {
|
|
924
|
+
const { internal } = await ctx.getCorsair();
|
|
925
|
+
return {
|
|
926
|
+
multiTenancy: internal.multiTenancy,
|
|
927
|
+
pluginCount: internal.plugins.length,
|
|
928
|
+
hasDatabase: !!internal.database,
|
|
929
|
+
cwd: ctx.cwd
|
|
930
|
+
};
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// src/server/handlers/tenants.ts
|
|
934
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
935
|
+
function asDb2(internalDatabase) {
|
|
936
|
+
const wrapped = internalDatabase;
|
|
937
|
+
const candidate = wrapped.db ?? internalDatabase;
|
|
938
|
+
if (!candidate || typeof candidate !== "object" || !("selectFrom" in candidate && typeof candidate.selectFrom === "function") || !("insertInto" in candidate && typeof candidate.insertInto === "function")) {
|
|
939
|
+
throw new Error("Could not access kysely db handle.");
|
|
940
|
+
}
|
|
941
|
+
return candidate;
|
|
942
|
+
}
|
|
943
|
+
function getAuthPluginIds(internal) {
|
|
944
|
+
return internal.plugins.filter((plugin) => {
|
|
945
|
+
const options = plugin.options;
|
|
946
|
+
return typeof options?.authType === "string";
|
|
947
|
+
}).map((plugin) => plugin.id);
|
|
948
|
+
}
|
|
949
|
+
var listTenants = async (ctx) => {
|
|
950
|
+
const { internal } = await ctx.getCorsair();
|
|
951
|
+
if (!internal.multiTenancy) {
|
|
952
|
+
return { tenants: ["default"] };
|
|
953
|
+
}
|
|
954
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
955
|
+
const db = asDb2(internal.database);
|
|
956
|
+
const rows = await db.selectFrom("corsair_accounts").selectAll().execute();
|
|
957
|
+
const ids = /* @__PURE__ */ new Set(["default"]);
|
|
958
|
+
for (const row of rows) {
|
|
959
|
+
const tenantId = row.tenant_id;
|
|
960
|
+
if (typeof tenantId === "string" && tenantId.trim()) {
|
|
961
|
+
ids.add(tenantId);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return { tenants: Array.from(ids).sort() };
|
|
965
|
+
};
|
|
966
|
+
var createTenant = async (ctx) => {
|
|
967
|
+
const body = await readJsonBody(ctx.req);
|
|
968
|
+
const tenantId = String(body.tenantId ?? "").trim();
|
|
969
|
+
if (!tenantId) {
|
|
970
|
+
throw new Error("tenantId is required.");
|
|
971
|
+
}
|
|
972
|
+
if (tenantId === "main") {
|
|
973
|
+
throw new Error('"main" is reserved. Pick another tenant id.');
|
|
974
|
+
}
|
|
975
|
+
const { internal, resolveClient } = await ctx.getCorsair();
|
|
976
|
+
if (!internal.multiTenancy) {
|
|
977
|
+
throw new Error("Multi-tenancy is not enabled for this Corsair instance.");
|
|
978
|
+
}
|
|
979
|
+
if (!internal.database) throw new Error("No database configured.");
|
|
980
|
+
const db = asDb2(internal.database);
|
|
981
|
+
const now = /* @__PURE__ */ new Date();
|
|
982
|
+
const authPluginIds = getAuthPluginIds(internal);
|
|
983
|
+
if (authPluginIds.length === 0) {
|
|
984
|
+
return { ok: true, created: false };
|
|
985
|
+
}
|
|
986
|
+
const integrations = await db.selectFrom("corsair_integrations").selectAll().execute();
|
|
987
|
+
const integrationByName = /* @__PURE__ */ new Map();
|
|
988
|
+
for (const row of integrations) {
|
|
989
|
+
const name = row.name;
|
|
990
|
+
if (typeof name === "string") integrationByName.set(name, row);
|
|
991
|
+
}
|
|
992
|
+
for (const pluginId of authPluginIds) {
|
|
993
|
+
if (integrationByName.has(pluginId)) continue;
|
|
994
|
+
const id = randomUUID2();
|
|
995
|
+
const row = {
|
|
996
|
+
id,
|
|
997
|
+
name: pluginId,
|
|
998
|
+
config: {},
|
|
999
|
+
created_at: now,
|
|
1000
|
+
updated_at: now
|
|
1001
|
+
};
|
|
1002
|
+
await db.insertInto("corsair_integrations").values(row).execute();
|
|
1003
|
+
integrationByName.set(pluginId, row);
|
|
1004
|
+
}
|
|
1005
|
+
const existingAccounts = await db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).execute();
|
|
1006
|
+
const accountByIntegrationId = /* @__PURE__ */ new Map();
|
|
1007
|
+
for (const row of existingAccounts) {
|
|
1008
|
+
const integrationId = row.integration_id;
|
|
1009
|
+
if (typeof integrationId === "string") {
|
|
1010
|
+
accountByIntegrationId.set(integrationId, row);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
let createdAny = false;
|
|
1014
|
+
const pluginsMissingDek = /* @__PURE__ */ new Set();
|
|
1015
|
+
for (const pluginId of authPluginIds) {
|
|
1016
|
+
const integration = integrationByName.get(pluginId);
|
|
1017
|
+
const integrationId = integration?.id;
|
|
1018
|
+
if (typeof integrationId !== "string") continue;
|
|
1019
|
+
const existing = accountByIntegrationId.get(integrationId);
|
|
1020
|
+
if (!existing) {
|
|
1021
|
+
createdAny = true;
|
|
1022
|
+
pluginsMissingDek.add(pluginId);
|
|
1023
|
+
await db.insertInto("corsair_accounts").values({
|
|
1024
|
+
id: randomUUID2(),
|
|
1025
|
+
tenant_id: tenantId,
|
|
1026
|
+
integration_id: integrationId,
|
|
1027
|
+
config: {},
|
|
1028
|
+
created_at: now,
|
|
1029
|
+
updated_at: now
|
|
1030
|
+
}).execute();
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (!existing.dek) {
|
|
1034
|
+
pluginsMissingDek.add(pluginId);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (pluginsMissingDek.size > 0) {
|
|
1038
|
+
const tenantClient = resolveClient(tenantId);
|
|
1039
|
+
for (const pluginId of pluginsMissingDek) {
|
|
1040
|
+
const pluginNamespace = tenantClient[pluginId];
|
|
1041
|
+
const keyNamespace = pluginNamespace?.keys;
|
|
1042
|
+
const issueDek = keyNamespace?.issue_new_dek;
|
|
1043
|
+
if (typeof issueDek === "function") {
|
|
1044
|
+
await issueDek();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return { ok: true, created: createdAny };
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// src/server/router.ts
|
|
1052
|
+
var routes = [
|
|
1053
|
+
{ method: "GET", path: "/api/status", handler: getStatus },
|
|
1054
|
+
{ method: "GET", path: "/api/plugins", handler: listPlugins },
|
|
1055
|
+
{ method: "POST", path: "/api/plugins/setup", handler: setupPlugin },
|
|
1056
|
+
{ method: "GET", path: "/api/tenants", handler: listTenants },
|
|
1057
|
+
{ method: "POST", path: "/api/tenants/create", handler: createTenant },
|
|
1058
|
+
{ method: "POST", path: "/api/operations/list", handler: listOperations },
|
|
1059
|
+
{
|
|
1060
|
+
method: "POST",
|
|
1061
|
+
path: "/api/operations/schema",
|
|
1062
|
+
handler: schemaForOperation
|
|
1063
|
+
},
|
|
1064
|
+
{ method: "POST", path: "/api/operations/run", handler: runOperation },
|
|
1065
|
+
{ method: "POST", path: "/api/operations/script", handler: runScript },
|
|
1066
|
+
{ method: "POST", path: "/api/credentials/get", handler: getCredentials },
|
|
1067
|
+
{ method: "POST", path: "/api/credentials/set", handler: setCredentials },
|
|
1068
|
+
{ method: "POST", path: "/api/auth/start", handler: startOAuth },
|
|
1069
|
+
{ method: "POST", path: "/api/auth/exchange", handler: exchangeOAuth },
|
|
1070
|
+
{ method: "GET", path: "/api/db/tables", handler: listDbTables },
|
|
1071
|
+
{ method: "POST", path: "/api/db/rows", handler: listDbRows },
|
|
1072
|
+
{ method: "POST", path: "/api/db/entities/query", handler: queryEntityData },
|
|
1073
|
+
{ method: "GET", path: "/api/db/permissions", handler: listPermissions }
|
|
1074
|
+
];
|
|
1075
|
+
async function handleApi(req, res, baseCtx) {
|
|
1076
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1077
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
1078
|
+
const route = routes.find(
|
|
1079
|
+
(r) => r.method === method && r.path === url.pathname
|
|
1080
|
+
);
|
|
1081
|
+
if (!route) {
|
|
1082
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
1083
|
+
res.end(
|
|
1084
|
+
JSON.stringify({ error: `No such endpoint: ${method} ${url.pathname}` })
|
|
1085
|
+
);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const ctx = { req, res, url, ...baseCtx };
|
|
1089
|
+
try {
|
|
1090
|
+
const result = await route.handler(ctx);
|
|
1091
|
+
if (res.writableEnded || res.headersSent) return;
|
|
1092
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1093
|
+
res.end(JSON.stringify(result ?? null));
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1096
|
+
if (!res.headersSent) {
|
|
1097
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
1098
|
+
res.end(JSON.stringify({ error: message }));
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async function readJsonBody(req) {
|
|
1103
|
+
return new Promise((resolve2, reject) => {
|
|
1104
|
+
const chunks = [];
|
|
1105
|
+
req.on("data", (c) => chunks.push(c));
|
|
1106
|
+
req.on("end", () => {
|
|
1107
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1108
|
+
if (!raw) return resolve2({});
|
|
1109
|
+
try {
|
|
1110
|
+
const parsed = JSON.parse(raw);
|
|
1111
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1112
|
+
resolve2(parsed);
|
|
1113
|
+
} else {
|
|
1114
|
+
resolve2({});
|
|
1115
|
+
}
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
reject(new Error(`Invalid JSON body: ${e.message}`));
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
req.on("error", reject);
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/server/serve-static.ts
|
|
1125
|
+
import { createReadStream, existsSync, statSync } from "fs";
|
|
1126
|
+
import { extname, join, normalize, resolve } from "path";
|
|
1127
|
+
import { fileURLToPath } from "url";
|
|
1128
|
+
var MIME = {
|
|
1129
|
+
".html": "text/html; charset=utf-8",
|
|
1130
|
+
".js": "application/javascript; charset=utf-8",
|
|
1131
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
1132
|
+
".css": "text/css; charset=utf-8",
|
|
1133
|
+
".json": "application/json; charset=utf-8",
|
|
1134
|
+
".svg": "image/svg+xml",
|
|
1135
|
+
".png": "image/png",
|
|
1136
|
+
".jpg": "image/jpeg",
|
|
1137
|
+
".ico": "image/x-icon",
|
|
1138
|
+
".map": "application/json; charset=utf-8",
|
|
1139
|
+
".woff": "font/woff",
|
|
1140
|
+
".woff2": "font/woff2"
|
|
1141
|
+
};
|
|
1142
|
+
function resolveWebRoot() {
|
|
1143
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
1144
|
+
const built = resolve(here, "../web");
|
|
1145
|
+
if (existsSync(join(built, "index.html"))) return built;
|
|
1146
|
+
const viaDev = resolve(here, "../../dist/web");
|
|
1147
|
+
return viaDev;
|
|
1148
|
+
}
|
|
1149
|
+
function createStaticServer(webRoot) {
|
|
1150
|
+
const root = webRoot ?? resolveWebRoot();
|
|
1151
|
+
return function serveStatic(req, res) {
|
|
1152
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
1153
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1154
|
+
let pathname = decodeURIComponent(url.pathname);
|
|
1155
|
+
if (pathname === "/" || pathname === "") pathname = "/index.html";
|
|
1156
|
+
const unsafe = join(root, pathname);
|
|
1157
|
+
const safe = normalize(unsafe);
|
|
1158
|
+
if (!safe.startsWith(root)) {
|
|
1159
|
+
res.writeHead(403).end("forbidden");
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
let filePath = safe;
|
|
1163
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
1164
|
+
filePath = join(root, "index.html");
|
|
1165
|
+
if (!existsSync(filePath)) {
|
|
1166
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
1167
|
+
res.end(
|
|
1168
|
+
`Corsair Studio assets not found at ${root}.
|
|
1169
|
+
If running from source, run \`pnpm --filter @corsair-dev/studio build:web\` first.`
|
|
1170
|
+
);
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const ext = extname(filePath).toLowerCase();
|
|
1175
|
+
const type = MIME[ext] ?? "application/octet-stream";
|
|
1176
|
+
res.writeHead(200, { "content-type": type });
|
|
1177
|
+
if (req.method === "HEAD") {
|
|
1178
|
+
res.end();
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
createReadStream(filePath).pipe(res);
|
|
1182
|
+
return true;
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/server/index.ts
|
|
1187
|
+
async function startStudio(options = {}) {
|
|
1188
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1189
|
+
const port = options.port ?? 4317;
|
|
1190
|
+
const host = options.host ?? "127.0.0.1";
|
|
1191
|
+
const open = options.open ?? true;
|
|
1192
|
+
let handle;
|
|
1193
|
+
try {
|
|
1194
|
+
handle = await loadCorsair(cwd);
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
console.error("[corsair:studio] Failed to load your corsair instance:");
|
|
1197
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
1198
|
+
throw err;
|
|
1199
|
+
}
|
|
1200
|
+
const configPath = findCorsairConfigPath(cwd);
|
|
1201
|
+
let refreshTimer = null;
|
|
1202
|
+
let refreshing = null;
|
|
1203
|
+
let refreshQueued = false;
|
|
1204
|
+
const uiEventClients = /* @__PURE__ */ new Set();
|
|
1205
|
+
const notifyUiCorsairChanged = () => {
|
|
1206
|
+
const message = `event: corsair-changed
|
|
1207
|
+
data: ${JSON.stringify({ ts: Date.now() })}
|
|
1208
|
+
|
|
1209
|
+
`;
|
|
1210
|
+
for (const client of uiEventClients) {
|
|
1211
|
+
client.write(message);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
const refreshCorsair = async () => {
|
|
1215
|
+
if (refreshing) {
|
|
1216
|
+
refreshQueued = true;
|
|
1217
|
+
return refreshing;
|
|
1218
|
+
}
|
|
1219
|
+
refreshing = (async () => {
|
|
1220
|
+
do {
|
|
1221
|
+
refreshQueued = false;
|
|
1222
|
+
try {
|
|
1223
|
+
const nextHandle = await loadCorsair(cwd, { force: true });
|
|
1224
|
+
handle = nextHandle;
|
|
1225
|
+
console.log(
|
|
1226
|
+
"[corsair:studio] Reloaded Corsair instance after change."
|
|
1227
|
+
);
|
|
1228
|
+
console.log(
|
|
1229
|
+
`[corsair:studio] plugins: ${handle.internal.plugins.length}`
|
|
1230
|
+
);
|
|
1231
|
+
console.log(
|
|
1232
|
+
`[corsair:studio] multiTenant: ${handle.internal.multiTenancy ? "yes" : "no"}`
|
|
1233
|
+
);
|
|
1234
|
+
notifyUiCorsairChanged();
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
console.error(
|
|
1237
|
+
"[corsair:studio] Failed to reload Corsair instance after change:"
|
|
1238
|
+
);
|
|
1239
|
+
console.error(
|
|
1240
|
+
` ${err instanceof Error ? err.message : String(err)}`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
} while (refreshQueued);
|
|
1244
|
+
})().finally(() => {
|
|
1245
|
+
refreshing = null;
|
|
1246
|
+
});
|
|
1247
|
+
return refreshing;
|
|
1248
|
+
};
|
|
1249
|
+
const onConfigFileChange = (curr, prev) => {
|
|
1250
|
+
if (curr.mtimeMs === prev.mtimeMs && curr.size === prev.size) return;
|
|
1251
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
1252
|
+
refreshTimer = setTimeout(() => {
|
|
1253
|
+
void refreshCorsair();
|
|
1254
|
+
}, 120);
|
|
1255
|
+
};
|
|
1256
|
+
if (configPath) {
|
|
1257
|
+
fs.watchFile(configPath, { interval: 250 }, onConfigFileChange);
|
|
1258
|
+
console.log(`[corsair:studio] watching: ${configPath}`);
|
|
1259
|
+
} else {
|
|
1260
|
+
console.log(
|
|
1261
|
+
"[corsair:studio] warning: could not find corsair config file."
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const serveStatic = createStaticServer(options.webRoot);
|
|
1265
|
+
const server = http2.createServer(async (req, res) => {
|
|
1266
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1267
|
+
const url2 = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
1268
|
+
if (url2.pathname === "/api/events") {
|
|
1269
|
+
res.writeHead(200, {
|
|
1270
|
+
"content-type": "text/event-stream",
|
|
1271
|
+
"cache-control": "no-cache, no-transform",
|
|
1272
|
+
connection: "keep-alive"
|
|
1273
|
+
});
|
|
1274
|
+
res.write(": connected\n\n");
|
|
1275
|
+
uiEventClients.add(res);
|
|
1276
|
+
req.on("close", () => {
|
|
1277
|
+
uiEventClients.delete(res);
|
|
1278
|
+
});
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (url2.pathname.startsWith("/api/")) {
|
|
1282
|
+
await handleApi(req, res, {
|
|
1283
|
+
cwd,
|
|
1284
|
+
getCorsair: async () => handle
|
|
1285
|
+
});
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (serveStatic(req, res)) return;
|
|
1289
|
+
res.writeHead(404).end("not found");
|
|
1290
|
+
});
|
|
1291
|
+
await new Promise((resolve2, reject) => {
|
|
1292
|
+
server.once("error", reject);
|
|
1293
|
+
server.listen(port, host, () => resolve2());
|
|
1294
|
+
});
|
|
1295
|
+
const url = `http://${host}:${port}`;
|
|
1296
|
+
console.log(`[corsair:studio] \u2192 ${url}`);
|
|
1297
|
+
console.log(`[corsair:studio] project: ${cwd}`);
|
|
1298
|
+
console.log(`[corsair:studio] plugins: ${handle.internal.plugins.length}`);
|
|
1299
|
+
console.log(
|
|
1300
|
+
`[corsair:studio] multiTenant: ${handle.internal.multiTenancy ? "yes" : "no"}`
|
|
1301
|
+
);
|
|
1302
|
+
if (open) {
|
|
1303
|
+
openInBrowser(url).catch(() => {
|
|
1304
|
+
console.log("[corsair:studio] Could not open browser automatically.");
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
port,
|
|
1309
|
+
url,
|
|
1310
|
+
close: async () => {
|
|
1311
|
+
if (refreshTimer) {
|
|
1312
|
+
clearTimeout(refreshTimer);
|
|
1313
|
+
refreshTimer = null;
|
|
1314
|
+
}
|
|
1315
|
+
if (configPath) {
|
|
1316
|
+
fs.unwatchFile(configPath, onConfigFileChange);
|
|
1317
|
+
}
|
|
1318
|
+
for (const client of uiEventClients) {
|
|
1319
|
+
client.end();
|
|
1320
|
+
}
|
|
1321
|
+
uiEventClients.clear();
|
|
1322
|
+
await new Promise(
|
|
1323
|
+
(resolve2, reject) => server.close((err) => err ? reject(err) : resolve2())
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
async function openInBrowser(url) {
|
|
1329
|
+
const { spawn } = await import("child_process");
|
|
1330
|
+
const platform = process.platform;
|
|
1331
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
1332
|
+
const args = platform === "win32" ? ["/c", "start", '""', url] : [url];
|
|
1333
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
1334
|
+
child.unref();
|
|
1335
|
+
}
|
|
1336
|
+
export {
|
|
1337
|
+
startStudio as start,
|
|
1338
|
+
startStudio
|
|
1339
|
+
};
|