fbi-proxy 1.9.0 → 1.10.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 +71 -0
- package/dist/cli.js +772 -29
- package/package.json +16 -10
- package/release/fbi-proxy-linux-arm64 +0 -0
- package/release/fbi-proxy-linux-x64 +0 -0
- package/release/fbi-proxy-macos-arm64 +0 -0
- package/release/fbi-proxy-macos-x64 +0 -0
- package/release/fbi-proxy-windows-arm64.exe +0 -0
- package/release/fbi-proxy-windows-x64.exe +0 -0
- package/rs/fbi-proxy.rs +123 -93
- package/rs/lib.rs +12 -0
- package/rs/routes.rs +976 -0
- package/ts/auth/authConfig.ts +141 -0
- package/ts/auth/caddyfileGen.test.ts +156 -0
- package/ts/auth/caddyfileGen.ts +142 -0
- package/ts/auth/downloadCaddy.test.ts +131 -0
- package/ts/auth/downloadCaddy.ts +213 -0
- package/ts/auth/setupWizard.ts +183 -0
- package/ts/auth/spawnCaddy.ts +125 -0
- package/ts/auth/spawnFbiAuth.ts +43 -0
- package/ts/buildFbiProxy.ts +3 -11
- package/ts/cli.ts +190 -7
- package/ts/dSpawn.ts +4 -9
- package/ts/getProxyFilename.ts +11 -9
- package/ts/routes.test.ts +182 -0
- package/ts/routes.ts +238 -0
package/dist/cli.js
CHANGED
|
@@ -132,7 +132,7 @@ async function hotMemo(fn, args = [], key = `_HOTMEMO_${g["_HOTMEMO_SALT_"]}_${S
|
|
|
132
132
|
hotMemo.cache = cache;
|
|
133
133
|
|
|
134
134
|
// ts/cli.ts
|
|
135
|
-
import
|
|
135
|
+
import path3 from "path";
|
|
136
136
|
|
|
137
137
|
// node_modules/yargs/lib/platform-shims/esm.mjs
|
|
138
138
|
import { notStrictEqual, strictEqual } from "assert";
|
|
@@ -1414,8 +1414,7 @@ var parser = new YargsParser({
|
|
|
1414
1414
|
require: (path) => {
|
|
1415
1415
|
if (true) {
|
|
1416
1416
|
return __require(path);
|
|
1417
|
-
}
|
|
1418
|
-
;
|
|
1417
|
+
}
|
|
1419
1418
|
}
|
|
1420
1419
|
});
|
|
1421
1420
|
var yargsParser = function Parser(args, opts) {
|
|
@@ -5035,10 +5034,7 @@ function tsaComposer(slotParser, compose = (...zipped) => zipped.join("")) {
|
|
|
5035
5034
|
}
|
|
5036
5035
|
|
|
5037
5036
|
// ts/dSpawn.ts
|
|
5038
|
-
var dSpawn = ({
|
|
5039
|
-
cwd = process.cwd(),
|
|
5040
|
-
env: env2 = process.env
|
|
5041
|
-
} = {}) => tsaComposer((slot) => typeof slot === "string" ? {
|
|
5037
|
+
var dSpawn = ({ cwd = process.cwd(), env: env2 = process.env } = {}) => tsaComposer((slot) => typeof slot === "string" ? {
|
|
5042
5038
|
raw: String(slot)
|
|
5043
5039
|
} : slot, (...slots) => {
|
|
5044
5040
|
try {
|
|
@@ -5065,11 +5061,7 @@ var dSpawn = ({
|
|
|
5065
5061
|
const codePromise = new Promise((resolve5) => {
|
|
5066
5062
|
p.on("exit", (code) => resolve5(code || 0));
|
|
5067
5063
|
});
|
|
5068
|
-
const mainPromise = Promise.all([
|
|
5069
|
-
outPromise,
|
|
5070
|
-
errPromise,
|
|
5071
|
-
codePromise
|
|
5072
|
-
]).then(([out, err, code]) => ({ out, err, code }));
|
|
5064
|
+
const mainPromise = Promise.all([outPromise, errPromise, codePromise]).then(([out, err, code]) => ({ out, err, code }));
|
|
5073
5065
|
const result = new Proxy(mainPromise, {
|
|
5074
5066
|
get(target, prop) {
|
|
5075
5067
|
if (prop === "out")
|
|
@@ -5118,10 +5110,7 @@ var $ = Object.assign(dSpawn(), {
|
|
|
5118
5110
|
|
|
5119
5111
|
// ts/buildFbiProxy.ts
|
|
5120
5112
|
if (false) {}
|
|
5121
|
-
async function getFbiProxyBinary({
|
|
5122
|
-
rebuild = false,
|
|
5123
|
-
originalCwd = ""
|
|
5124
|
-
} = {}) {
|
|
5113
|
+
async function getFbiProxyBinary({ rebuild = false, originalCwd = "" } = {}) {
|
|
5125
5114
|
const isWin = process.platform === "win32";
|
|
5126
5115
|
const binaryName = getFbiProxyFilename();
|
|
5127
5116
|
const binarySuffix = isWin ? ".exe" : "";
|
|
@@ -5157,10 +5146,7 @@ async function getFbiProxyBinary({
|
|
|
5157
5146
|
|
|
5158
5147
|
// ts/dSpawn.ts
|
|
5159
5148
|
import { spawn as spawn2 } from "child_process";
|
|
5160
|
-
var dSpawn2 = ({
|
|
5161
|
-
cwd = process.cwd(),
|
|
5162
|
-
env: env2 = process.env
|
|
5163
|
-
} = {}) => tsaComposer((slot) => typeof slot === "string" ? {
|
|
5149
|
+
var dSpawn2 = ({ cwd = process.cwd(), env: env2 = process.env } = {}) => tsaComposer((slot) => typeof slot === "string" ? {
|
|
5164
5150
|
raw: String(slot)
|
|
5165
5151
|
} : slot, (...slots) => {
|
|
5166
5152
|
try {
|
|
@@ -5187,11 +5173,7 @@ var dSpawn2 = ({
|
|
|
5187
5173
|
const codePromise = new Promise((resolve5) => {
|
|
5188
5174
|
p.on("exit", (code) => resolve5(code || 0));
|
|
5189
5175
|
});
|
|
5190
|
-
const mainPromise = Promise.all([
|
|
5191
|
-
outPromise,
|
|
5192
|
-
errPromise,
|
|
5193
|
-
codePromise
|
|
5194
|
-
]).then(([out, err, code]) => ({ out, err, code }));
|
|
5176
|
+
const mainPromise = Promise.all([outPromise, errPromise, codePromise]).then(([out, err, code]) => ({ out, err, code }));
|
|
5195
5177
|
const result = new Proxy(mainPromise, {
|
|
5196
5178
|
get(target, prop) {
|
|
5197
5179
|
if (prop === "out")
|
|
@@ -5238,14 +5220,686 @@ var $2 = Object.assign(dSpawn2(), {
|
|
|
5238
5220
|
cwd: (path2) => dSpawn2({ cwd: path2 })
|
|
5239
5221
|
});
|
|
5240
5222
|
|
|
5223
|
+
// ts/auth/authConfig.ts
|
|
5224
|
+
import { homedir } from "node:os";
|
|
5225
|
+
import { join, dirname as dirname3 } from "node:path";
|
|
5226
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
5227
|
+
import { mkdir, writeFile as writeFile2, chmod as chmod2, readFile } from "node:fs/promises";
|
|
5228
|
+
import { randomBytes } from "node:crypto";
|
|
5229
|
+
function defaultConfigPath() {
|
|
5230
|
+
return process.env.FBI_AUTH_CONFIG_PATH ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "fbi-proxy", "auth.json");
|
|
5231
|
+
}
|
|
5232
|
+
async function readConfigOrNull(path2 = defaultConfigPath()) {
|
|
5233
|
+
if (!existsSync2(path2))
|
|
5234
|
+
return null;
|
|
5235
|
+
try {
|
|
5236
|
+
return JSON.parse(await readFile(path2, "utf8"));
|
|
5237
|
+
} catch {
|
|
5238
|
+
return null;
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
async function writeConfig(cfg, path2 = defaultConfigPath()) {
|
|
5242
|
+
await mkdir(dirname3(path2), { recursive: true });
|
|
5243
|
+
await writeFile2(path2, JSON.stringify(cfg, null, 2), "utf8");
|
|
5244
|
+
await chmod2(path2, 384);
|
|
5245
|
+
}
|
|
5246
|
+
function configFromEnv(domain) {
|
|
5247
|
+
const provider = process.env.FBI_AUTH_PROVIDER ?? "google";
|
|
5248
|
+
const clientId = process.env.FBI_AUTH_CLIENT_ID;
|
|
5249
|
+
const firebaseProjectId = process.env.FBI_AUTH_FIREBASE_PROJECT_ID;
|
|
5250
|
+
if (provider === "firebase") {
|
|
5251
|
+
if (!firebaseProjectId)
|
|
5252
|
+
return null;
|
|
5253
|
+
} else {
|
|
5254
|
+
if (!clientId)
|
|
5255
|
+
return null;
|
|
5256
|
+
}
|
|
5257
|
+
const d = domain.startsWith(".") ? domain.slice(1) : domain;
|
|
5258
|
+
return {
|
|
5259
|
+
version: 1,
|
|
5260
|
+
domain: d,
|
|
5261
|
+
cookieDomain: `.${d}`,
|
|
5262
|
+
ssoHost: `sso.${d}`,
|
|
5263
|
+
provider,
|
|
5264
|
+
clientId,
|
|
5265
|
+
clientSecret: process.env.FBI_AUTH_CLIENT_SECRET,
|
|
5266
|
+
firebase: firebaseProjectId ? {
|
|
5267
|
+
projectId: firebaseProjectId,
|
|
5268
|
+
apiKey: process.env.FBI_AUTH_FIREBASE_API_KEY,
|
|
5269
|
+
authDomain: process.env.FBI_AUTH_FIREBASE_AUTH_DOMAIN
|
|
5270
|
+
} : undefined,
|
|
5271
|
+
sessionSecret: process.env.FBI_AUTH_SESSION_SECRET ?? randomBytes(32).toString("base64url"),
|
|
5272
|
+
allowlist: parseAllowlistEnv()
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
function parseAllowlistEnv() {
|
|
5276
|
+
const emails = process.env.FBI_AUTH_ALLOW_EMAILS?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5277
|
+
const domains = process.env.FBI_AUTH_ALLOW_DOMAINS?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5278
|
+
const anySignedIn = process.env.FBI_AUTH_ALLOW_ANY === "true";
|
|
5279
|
+
if (!emails?.length && !domains?.length && !anySignedIn) {
|
|
5280
|
+
return { anySignedIn: true };
|
|
5281
|
+
}
|
|
5282
|
+
return { emails, domains, anySignedIn };
|
|
5283
|
+
}
|
|
5284
|
+
function helpfulSetupMessage(domain, path2) {
|
|
5285
|
+
return [
|
|
5286
|
+
"",
|
|
5287
|
+
"fbi-auth requires a config file but none was found.",
|
|
5288
|
+
`Expected at: ${path2}`,
|
|
5289
|
+
"",
|
|
5290
|
+
"Quick setup (Phase 1 — manual; setup wizard arrives in Phase 2):",
|
|
5291
|
+
"",
|
|
5292
|
+
" 1. Create a Google OAuth Web client at https://console.cloud.google.com/apis/credentials",
|
|
5293
|
+
` - Authorized redirect URI: https://sso.${domain}/callback`,
|
|
5294
|
+
` - Authorized JS origin: https://sso.${domain}`,
|
|
5295
|
+
"",
|
|
5296
|
+
" 2. Either write the config file directly:",
|
|
5297
|
+
` mkdir -p $(dirname ${path2}) && cat > ${path2} <<EOF`,
|
|
5298
|
+
" {",
|
|
5299
|
+
' "version": 1,',
|
|
5300
|
+
` "domain": "${domain}",`,
|
|
5301
|
+
` "cookieDomain": ".${domain}",`,
|
|
5302
|
+
` "ssoHost": "sso.${domain}",`,
|
|
5303
|
+
' "provider": "google",',
|
|
5304
|
+
' "clientId": "<your-client-id>",',
|
|
5305
|
+
' "clientSecret": "<your-client-secret>",',
|
|
5306
|
+
' "sessionSecret": "<32+ random chars, base64url preferred>",',
|
|
5307
|
+
' "allowlist": { "anySignedIn": true }',
|
|
5308
|
+
" }",
|
|
5309
|
+
" EOF",
|
|
5310
|
+
"",
|
|
5311
|
+
" 3. Or use env vars (auto-creates the config on first run):",
|
|
5312
|
+
` FBI_AUTH_CLIENT_ID=... FBI_AUTH_CLIENT_SECRET=... bunx fbi-proxy --with-auth --domain ${domain}`,
|
|
5313
|
+
""
|
|
5314
|
+
].join(`
|
|
5315
|
+
`);
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5318
|
+
// ts/auth/spawnFbiAuth.ts
|
|
5319
|
+
import path2 from "node:path";
|
|
5320
|
+
|
|
5321
|
+
// node_modules/get-port/index.js
|
|
5322
|
+
import net2 from "node:net";
|
|
5323
|
+
import os2 from "node:os";
|
|
5324
|
+
|
|
5325
|
+
class Locked2 extends Error {
|
|
5326
|
+
constructor(port) {
|
|
5327
|
+
super(`${port} is locked`);
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
var lockedPorts2 = {
|
|
5331
|
+
old: new Set,
|
|
5332
|
+
young: new Set
|
|
5333
|
+
};
|
|
5334
|
+
var releaseOldLockedPortsIntervalMs2 = 1000 * 15;
|
|
5335
|
+
var timeout2;
|
|
5336
|
+
var getLocalHosts2 = () => {
|
|
5337
|
+
const interfaces = os2.networkInterfaces();
|
|
5338
|
+
const results = new Set([undefined, "0.0.0.0"]);
|
|
5339
|
+
for (const _interface of Object.values(interfaces)) {
|
|
5340
|
+
for (const config of _interface) {
|
|
5341
|
+
results.add(config.address);
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
return results;
|
|
5345
|
+
};
|
|
5346
|
+
var checkAvailablePort2 = (options) => new Promise((resolve5, reject) => {
|
|
5347
|
+
const server = net2.createServer();
|
|
5348
|
+
server.unref();
|
|
5349
|
+
server.on("error", reject);
|
|
5350
|
+
server.listen(options, () => {
|
|
5351
|
+
const { port } = server.address();
|
|
5352
|
+
server.close(() => {
|
|
5353
|
+
resolve5(port);
|
|
5354
|
+
});
|
|
5355
|
+
});
|
|
5356
|
+
});
|
|
5357
|
+
var getAvailablePort2 = async (options, hosts) => {
|
|
5358
|
+
if (options.host || options.port === 0) {
|
|
5359
|
+
return checkAvailablePort2(options);
|
|
5360
|
+
}
|
|
5361
|
+
for (const host of hosts) {
|
|
5362
|
+
try {
|
|
5363
|
+
await checkAvailablePort2({ port: options.port, host });
|
|
5364
|
+
} catch (error) {
|
|
5365
|
+
if (!["EADDRNOTAVAIL", "EINVAL"].includes(error.code)) {
|
|
5366
|
+
throw error;
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
return options.port;
|
|
5371
|
+
};
|
|
5372
|
+
var portCheckSequence2 = function* (ports) {
|
|
5373
|
+
if (ports) {
|
|
5374
|
+
yield* ports;
|
|
5375
|
+
}
|
|
5376
|
+
yield 0;
|
|
5377
|
+
};
|
|
5378
|
+
async function getPorts2(options) {
|
|
5379
|
+
let ports;
|
|
5380
|
+
let exclude = new Set;
|
|
5381
|
+
if (options) {
|
|
5382
|
+
if (options.port) {
|
|
5383
|
+
ports = typeof options.port === "number" ? [options.port] : options.port;
|
|
5384
|
+
}
|
|
5385
|
+
if (options.exclude) {
|
|
5386
|
+
const excludeIterable = options.exclude;
|
|
5387
|
+
if (typeof excludeIterable[Symbol.iterator] !== "function") {
|
|
5388
|
+
throw new TypeError("The `exclude` option must be an iterable.");
|
|
5389
|
+
}
|
|
5390
|
+
for (const element of excludeIterable) {
|
|
5391
|
+
if (typeof element !== "number") {
|
|
5392
|
+
throw new TypeError("Each item in the `exclude` option must be a number corresponding to the port you want excluded.");
|
|
5393
|
+
}
|
|
5394
|
+
if (!Number.isSafeInteger(element)) {
|
|
5395
|
+
throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`);
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
exclude = new Set(excludeIterable);
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
if (timeout2 === undefined) {
|
|
5402
|
+
timeout2 = setTimeout(() => {
|
|
5403
|
+
timeout2 = undefined;
|
|
5404
|
+
lockedPorts2.old = lockedPorts2.young;
|
|
5405
|
+
lockedPorts2.young = new Set;
|
|
5406
|
+
}, releaseOldLockedPortsIntervalMs2);
|
|
5407
|
+
if (timeout2.unref) {
|
|
5408
|
+
timeout2.unref();
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
const hosts = getLocalHosts2();
|
|
5412
|
+
for (const port of portCheckSequence2(ports)) {
|
|
5413
|
+
try {
|
|
5414
|
+
if (exclude.has(port)) {
|
|
5415
|
+
continue;
|
|
5416
|
+
}
|
|
5417
|
+
let availablePort = await getAvailablePort2({ ...options, port }, hosts);
|
|
5418
|
+
while (lockedPorts2.old.has(availablePort) || lockedPorts2.young.has(availablePort)) {
|
|
5419
|
+
if (port !== 0) {
|
|
5420
|
+
throw new Locked2(port);
|
|
5421
|
+
}
|
|
5422
|
+
availablePort = await getAvailablePort2({ ...options, port }, hosts);
|
|
5423
|
+
}
|
|
5424
|
+
lockedPorts2.young.add(availablePort);
|
|
5425
|
+
return availablePort;
|
|
5426
|
+
} catch (error) {
|
|
5427
|
+
if (!["EADDRINUSE", "EACCES"].includes(error.code) && !(error instanceof Locked2)) {
|
|
5428
|
+
throw error;
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
throw new Error("No available ports found");
|
|
5433
|
+
}
|
|
5434
|
+
|
|
5435
|
+
// ts/auth/spawnFbiAuth.ts
|
|
5436
|
+
async function spawnFbiAuth(opts) {
|
|
5437
|
+
const port = await getPorts2({ port: opts.preferredPort ?? 2433 });
|
|
5438
|
+
const entry = path2.resolve(import.meta.dir, "..", "..", "lib", "fbi-auth", "src", "server.ts");
|
|
5439
|
+
const proc = $.opt({
|
|
5440
|
+
env: {
|
|
5441
|
+
...process.env,
|
|
5442
|
+
FBI_AUTH_PORT: String(port),
|
|
5443
|
+
FBI_AUTH_CONFIG_PATH: opts.configPath
|
|
5444
|
+
}
|
|
5445
|
+
})`bun ${entry}`.process;
|
|
5446
|
+
proc.on("exit", (code) => {
|
|
5447
|
+
console.log(`[fbi-auth] exited with code ${code}`);
|
|
5448
|
+
});
|
|
5449
|
+
return {
|
|
5450
|
+
port,
|
|
5451
|
+
pid: proc.pid,
|
|
5452
|
+
kill: () => proc.kill?.()
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5455
|
+
|
|
5456
|
+
// ts/auth/setupWizard.ts
|
|
5457
|
+
import * as readline from "node:readline/promises";
|
|
5458
|
+
import { stdin, stdout } from "node:process";
|
|
5459
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
5460
|
+
async function runWizard(prompter, opts) {
|
|
5461
|
+
prompter.print("");
|
|
5462
|
+
prompter.print("fbi-auth setup wizard");
|
|
5463
|
+
prompter.print("─────────────────────");
|
|
5464
|
+
prompter.print("");
|
|
5465
|
+
const domain = await prompter.ask("Domain to gate", opts.domain);
|
|
5466
|
+
const cleanDomain = domain.replace(/^\.+/, "").trim();
|
|
5467
|
+
const providerIdx = await prompter.askChoice("Identity provider", [
|
|
5468
|
+
"Google OAuth (BYO client ID + secret)",
|
|
5469
|
+
"Firebase Auth (BYO project ID)",
|
|
5470
|
+
"Snolab default (zero-config; supported domains only)"
|
|
5471
|
+
]);
|
|
5472
|
+
const provider = providerIdx === 0 ? "google" : providerIdx === 1 ? "firebase" : "snolab";
|
|
5473
|
+
let clientId;
|
|
5474
|
+
let clientSecret;
|
|
5475
|
+
let firebase;
|
|
5476
|
+
if (provider === "google") {
|
|
5477
|
+
clientId = await prompter.ask("Google OAuth Client ID", opts.existing?.provider === "google" ? opts.existing.clientId : undefined);
|
|
5478
|
+
clientSecret = await prompter.ask("Google OAuth Client Secret", opts.existing?.provider === "google" ? opts.existing.clientSecret : undefined);
|
|
5479
|
+
prompter.print("");
|
|
5480
|
+
prompter.print(` → Add this redirect URI in Google Cloud Console: https://sso.${cleanDomain}/callback`);
|
|
5481
|
+
prompter.print("");
|
|
5482
|
+
} else if (provider === "firebase") {
|
|
5483
|
+
const projectId = await prompter.ask("Firebase Project ID", opts.existing?.firebase?.projectId);
|
|
5484
|
+
const apiKey = await prompter.ask("Firebase Web API Key (optional)", opts.existing?.firebase?.apiKey ?? "");
|
|
5485
|
+
const authDomain = await prompter.ask("Firebase Auth Domain", opts.existing?.firebase?.authDomain ?? `${projectId}.firebaseapp.com`);
|
|
5486
|
+
firebase = {
|
|
5487
|
+
projectId: projectId.trim(),
|
|
5488
|
+
apiKey: apiKey.trim() || undefined,
|
|
5489
|
+
authDomain: authDomain.trim() || undefined
|
|
5490
|
+
};
|
|
5491
|
+
} else {
|
|
5492
|
+
prompter.print("");
|
|
5493
|
+
prompter.print(` → Snolab default IdP — no credentials needed. Domain '${cleanDomain}'`);
|
|
5494
|
+
prompter.print(` will be checked against SNOLAB_SUPPORTED_DOMAINS at startup.`);
|
|
5495
|
+
prompter.print("");
|
|
5496
|
+
}
|
|
5497
|
+
const allowIdx = await prompter.askChoice("Allowlist policy", [
|
|
5498
|
+
"Anyone who completes sign-in",
|
|
5499
|
+
"Specific email addresses",
|
|
5500
|
+
"Specific email domain(s)"
|
|
5501
|
+
]);
|
|
5502
|
+
let allowlist = { anySignedIn: true };
|
|
5503
|
+
if (allowIdx === 1) {
|
|
5504
|
+
const raw = await prompter.ask("Allowed emails (comma-separated)");
|
|
5505
|
+
allowlist = {
|
|
5506
|
+
emails: raw.split(",").map((s) => s.trim()).filter(Boolean),
|
|
5507
|
+
anySignedIn: false
|
|
5508
|
+
};
|
|
5509
|
+
} else if (allowIdx === 2) {
|
|
5510
|
+
const raw = await prompter.ask("Allowed email domains (comma-separated)");
|
|
5511
|
+
allowlist = {
|
|
5512
|
+
domains: raw.split(",").map((s) => s.trim()).filter(Boolean),
|
|
5513
|
+
anySignedIn: false
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5516
|
+
const cfg = {
|
|
5517
|
+
version: 1,
|
|
5518
|
+
domain: cleanDomain,
|
|
5519
|
+
cookieDomain: `.${cleanDomain}`,
|
|
5520
|
+
ssoHost: `sso.${cleanDomain}`,
|
|
5521
|
+
provider,
|
|
5522
|
+
clientId,
|
|
5523
|
+
clientSecret,
|
|
5524
|
+
firebase,
|
|
5525
|
+
sessionSecret: opts.existing?.sessionSecret ?? randomBytes2(32).toString("base64url"),
|
|
5526
|
+
allowlist
|
|
5527
|
+
};
|
|
5528
|
+
prompter.print("");
|
|
5529
|
+
prompter.print("Config preview:");
|
|
5530
|
+
prompter.print(JSON.stringify(redact(cfg), null, 2));
|
|
5531
|
+
prompter.print("");
|
|
5532
|
+
return cfg;
|
|
5533
|
+
}
|
|
5534
|
+
function redact(c) {
|
|
5535
|
+
return {
|
|
5536
|
+
...c,
|
|
5537
|
+
clientSecret: c.clientSecret ? "***" : undefined,
|
|
5538
|
+
sessionSecret: "***"
|
|
5539
|
+
};
|
|
5540
|
+
}
|
|
5541
|
+
function readlinePrompter() {
|
|
5542
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
5543
|
+
return {
|
|
5544
|
+
async ask(question, defaultValue) {
|
|
5545
|
+
const hint = defaultValue !== undefined && defaultValue !== "" ? ` [${defaultValue}]` : "";
|
|
5546
|
+
const answer = (await rl.question(`? ${question}${hint}: `)).trim();
|
|
5547
|
+
return answer || defaultValue || "";
|
|
5548
|
+
},
|
|
5549
|
+
async askChoice(question, choices) {
|
|
5550
|
+
this.print(`? ${question}:`);
|
|
5551
|
+
choices.forEach((c, i) => this.print(` ${i + 1}) ${c}`));
|
|
5552
|
+
while (true) {
|
|
5553
|
+
const raw = (await rl.question("> ")).trim();
|
|
5554
|
+
const idx = Number(raw) - 1;
|
|
5555
|
+
if (Number.isInteger(idx) && idx >= 0 && idx < choices.length)
|
|
5556
|
+
return idx;
|
|
5557
|
+
this.print(` (enter 1-${choices.length})`);
|
|
5558
|
+
}
|
|
5559
|
+
},
|
|
5560
|
+
print(line) {
|
|
5561
|
+
stdout.write(line + `
|
|
5562
|
+
`);
|
|
5563
|
+
}
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
function isTty() {
|
|
5567
|
+
return Boolean(stdin.isTTY && stdout.isTTY);
|
|
5568
|
+
}
|
|
5569
|
+
|
|
5570
|
+
// ts/auth/caddyfileGen.ts
|
|
5571
|
+
import { homedir as homedir2 } from "node:os";
|
|
5572
|
+
import { dirname as dirname4, join as join2 } from "node:path";
|
|
5573
|
+
import { mkdir as mkdir2, writeFile as writeFile3, chmod as chmod3 } from "node:fs/promises";
|
|
5574
|
+
function generateCaddyfile(opts) {
|
|
5575
|
+
const domain = stripLeadingDot(opts.domain);
|
|
5576
|
+
const tlsMode = opts.tlsMode ?? "auto";
|
|
5577
|
+
const withAuth = opts.withAuth ?? false;
|
|
5578
|
+
const tlsStanza = tlsMode === "internal" ? ` tls internal
|
|
5579
|
+
` : "";
|
|
5580
|
+
const sections = [];
|
|
5581
|
+
if (opts.acmeEmail && opts.acmeEmail.trim() !== "") {
|
|
5582
|
+
sections.push(`{
|
|
5583
|
+
email ${opts.acmeEmail.trim()}
|
|
5584
|
+
}`);
|
|
5585
|
+
}
|
|
5586
|
+
if (withAuth) {
|
|
5587
|
+
const ssoHost = opts.ssoHost ?? `sso.${domain}`;
|
|
5588
|
+
const fbiAuthPort = opts.fbiAuthPort;
|
|
5589
|
+
if (fbiAuthPort === undefined) {
|
|
5590
|
+
throw new Error("generateCaddyfile: fbiAuthPort is required when withAuth is true");
|
|
5591
|
+
}
|
|
5592
|
+
sections.push(`${ssoHost} {
|
|
5593
|
+
` + ` reverse_proxy 127.0.0.1:${fbiAuthPort}
|
|
5594
|
+
` + tlsStanza + `}`);
|
|
5595
|
+
sections.push(`*.${domain} {
|
|
5596
|
+
` + ` @notauth not path /api/auth/* /login /callback /logout
|
|
5597
|
+
` + ` forward_auth @notauth 127.0.0.1:${fbiAuthPort} {
|
|
5598
|
+
` + ` uri /api/auth/verify
|
|
5599
|
+
` + ` copy_headers Remote-User Remote-Email Remote-Name
|
|
5600
|
+
` + ` header_up X-Forwarded-Host {host}
|
|
5601
|
+
` + ` header_up X-Forwarded-Uri {uri}
|
|
5602
|
+
` + ` }
|
|
5603
|
+
` + ` reverse_proxy 127.0.0.1:${opts.fbiProxyPort}
|
|
5604
|
+
` + tlsStanza + `}`);
|
|
5605
|
+
} else {
|
|
5606
|
+
sections.push(`*.${domain} {
|
|
5607
|
+
` + ` reverse_proxy 127.0.0.1:${opts.fbiProxyPort}
|
|
5608
|
+
` + tlsStanza + `}`);
|
|
5609
|
+
}
|
|
5610
|
+
return sections.join(`
|
|
5611
|
+
|
|
5612
|
+
`) + `
|
|
5613
|
+
`;
|
|
5614
|
+
}
|
|
5615
|
+
function defaultCaddyfilePath() {
|
|
5616
|
+
return process.env.FBI_PROXY_CADDYFILE_PATH ?? join2(process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), "fbi-proxy", "Caddyfile");
|
|
5617
|
+
}
|
|
5618
|
+
async function writeCaddyfile(opts, path3 = defaultCaddyfilePath()) {
|
|
5619
|
+
const content = generateCaddyfile(opts);
|
|
5620
|
+
await mkdir2(dirname4(path3), { recursive: true });
|
|
5621
|
+
await writeFile3(path3, content, "utf8");
|
|
5622
|
+
await chmod3(path3, 420);
|
|
5623
|
+
return { content, path: path3 };
|
|
5624
|
+
}
|
|
5625
|
+
function stripLeadingDot(d) {
|
|
5626
|
+
return d.startsWith(".") ? d.slice(1) : d;
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
// ts/auth/spawnCaddy.ts
|
|
5630
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
5631
|
+
import { access } from "node:fs/promises";
|
|
5632
|
+
import { constants as fsConstants } from "node:fs";
|
|
5633
|
+
import { homedir as homedir4 } from "node:os";
|
|
5634
|
+
import { join as join4 } from "node:path";
|
|
5635
|
+
|
|
5636
|
+
// ts/auth/downloadCaddy.ts
|
|
5637
|
+
import {
|
|
5638
|
+
mkdir as mkdir3,
|
|
5639
|
+
writeFile as writeFile4,
|
|
5640
|
+
rm,
|
|
5641
|
+
chmod as chmod4,
|
|
5642
|
+
rename,
|
|
5643
|
+
copyFile
|
|
5644
|
+
} from "node:fs/promises";
|
|
5645
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
5646
|
+
import { createHash } from "node:crypto";
|
|
5647
|
+
import { join as join3 } from "node:path";
|
|
5648
|
+
import { homedir as homedir3, tmpdir } from "node:os";
|
|
5649
|
+
function detectPlatform(platform = process.platform, arch = process.arch) {
|
|
5650
|
+
const osMap = {
|
|
5651
|
+
linux: "linux",
|
|
5652
|
+
darwin: "darwin",
|
|
5653
|
+
win32: "windows"
|
|
5654
|
+
};
|
|
5655
|
+
const archMap = {
|
|
5656
|
+
x64: "amd64",
|
|
5657
|
+
arm64: "arm64"
|
|
5658
|
+
};
|
|
5659
|
+
const os3 = osMap[platform];
|
|
5660
|
+
if (!os3)
|
|
5661
|
+
throw new Error(`Unsupported OS: ${platform}`);
|
|
5662
|
+
const mappedArch = archMap[arch];
|
|
5663
|
+
if (!mappedArch)
|
|
5664
|
+
throw new Error(`Unsupported arch: ${arch}`);
|
|
5665
|
+
return { os: os3, arch: mappedArch, ext: os3 === "windows" ? "zip" : "tar.gz" };
|
|
5666
|
+
}
|
|
5667
|
+
function buildAssetName(version, p) {
|
|
5668
|
+
const v = version.replace(/^v/, "");
|
|
5669
|
+
return `caddy_${v}_${p.os}_${p.arch}.${p.ext}`;
|
|
5670
|
+
}
|
|
5671
|
+
function buildAssetUrl(version, name) {
|
|
5672
|
+
const tag = version.startsWith("v") ? version : `v${version}`;
|
|
5673
|
+
return `https://github.com/caddyserver/caddy/releases/download/${tag}/${name}`;
|
|
5674
|
+
}
|
|
5675
|
+
function buildChecksumsUrl(version) {
|
|
5676
|
+
const v = version.replace(/^v/, "");
|
|
5677
|
+
const tag = version.startsWith("v") ? version : `v${version}`;
|
|
5678
|
+
return `https://github.com/caddyserver/caddy/releases/download/${tag}/caddy_${v}_checksums.txt`;
|
|
5679
|
+
}
|
|
5680
|
+
async function fetchLatestVersion(signal) {
|
|
5681
|
+
const res = await fetch("https://api.github.com/repos/caddyserver/caddy/releases/latest", { signal, headers: { "User-Agent": "fbi-proxy" } });
|
|
5682
|
+
if (!res.ok) {
|
|
5683
|
+
throw new Error(`GitHub API ${res.status} ${res.statusText} for latest release`);
|
|
5684
|
+
}
|
|
5685
|
+
const data = await res.json();
|
|
5686
|
+
if (!data.tag_name)
|
|
5687
|
+
throw new Error("GitHub API: response is missing tag_name");
|
|
5688
|
+
return data.tag_name;
|
|
5689
|
+
}
|
|
5690
|
+
function parseChecksums(text) {
|
|
5691
|
+
const out = new Map;
|
|
5692
|
+
for (const line of text.split(/\r?\n/)) {
|
|
5693
|
+
const trimmed = line.trim();
|
|
5694
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
5695
|
+
continue;
|
|
5696
|
+
const m = trimmed.match(/^([a-fA-F0-9]+)\s+\*?(.+)$/);
|
|
5697
|
+
if (m)
|
|
5698
|
+
out.set(m[2].trim(), m[1].toLowerCase());
|
|
5699
|
+
}
|
|
5700
|
+
return out;
|
|
5701
|
+
}
|
|
5702
|
+
async function sha512OfPath(path3) {
|
|
5703
|
+
const hash = createHash("sha512");
|
|
5704
|
+
const f = Bun.file(path3);
|
|
5705
|
+
const stream = f.stream();
|
|
5706
|
+
for await (const chunk of stream)
|
|
5707
|
+
hash.update(chunk);
|
|
5708
|
+
return hash.digest("hex");
|
|
5709
|
+
}
|
|
5710
|
+
async function downloadCaddy(opts = {}) {
|
|
5711
|
+
const log = opts.log ?? ((m) => console.log(`[caddy-download] ${m}`));
|
|
5712
|
+
const platform = opts.platform ?? detectPlatform();
|
|
5713
|
+
const destDir = opts.destDir ?? join3(homedir3(), ".fbi-proxy", "bin");
|
|
5714
|
+
const binaryName = platform.os === "windows" ? "caddy.exe" : "caddy";
|
|
5715
|
+
const destPath = join3(destDir, binaryName);
|
|
5716
|
+
if (existsSync3(destPath)) {
|
|
5717
|
+
log(`already installed: ${destPath}`);
|
|
5718
|
+
return destPath;
|
|
5719
|
+
}
|
|
5720
|
+
const version = opts.version ?? await fetchLatestVersion(opts.signal);
|
|
5721
|
+
log(`platform: ${platform.os}/${platform.arch}, version: ${version}`);
|
|
5722
|
+
const assetName = buildAssetName(version, platform);
|
|
5723
|
+
const assetUrl = buildAssetUrl(version, assetName);
|
|
5724
|
+
const checksumsUrl = buildChecksumsUrl(version);
|
|
5725
|
+
log(`fetching checksums: ${checksumsUrl}`);
|
|
5726
|
+
const cksRes = await fetch(checksumsUrl, {
|
|
5727
|
+
signal: opts.signal,
|
|
5728
|
+
headers: { "User-Agent": "fbi-proxy" }
|
|
5729
|
+
});
|
|
5730
|
+
if (!cksRes.ok)
|
|
5731
|
+
throw new Error(`checksums fetch failed: ${cksRes.status} ${cksRes.statusText}`);
|
|
5732
|
+
const checksums = parseChecksums(await cksRes.text());
|
|
5733
|
+
const expectedSum = checksums.get(assetName);
|
|
5734
|
+
if (!expectedSum) {
|
|
5735
|
+
throw new Error(`no checksum entry for '${assetName}' — release may not include this platform`);
|
|
5736
|
+
}
|
|
5737
|
+
await mkdir3(destDir, { recursive: true });
|
|
5738
|
+
const tmpArchive = join3(tmpdir(), `fbi-proxy.${process.pid}.${assetName}`);
|
|
5739
|
+
log(`downloading: ${assetUrl}`);
|
|
5740
|
+
const dlRes = await fetch(assetUrl, {
|
|
5741
|
+
signal: opts.signal,
|
|
5742
|
+
headers: { "User-Agent": "fbi-proxy" }
|
|
5743
|
+
});
|
|
5744
|
+
if (!dlRes.ok)
|
|
5745
|
+
throw new Error(`download failed: ${dlRes.status} ${dlRes.statusText}`);
|
|
5746
|
+
const bytes = await dlRes.arrayBuffer();
|
|
5747
|
+
await writeFile4(tmpArchive, Buffer.from(bytes));
|
|
5748
|
+
log(`downloaded ${bytes.byteLength} bytes`);
|
|
5749
|
+
const actualSum = await sha512OfPath(tmpArchive);
|
|
5750
|
+
if (actualSum !== expectedSum) {
|
|
5751
|
+
await rm(tmpArchive, { force: true });
|
|
5752
|
+
throw new Error(`SHA-512 mismatch for ${assetName}
|
|
5753
|
+
expected: ${expectedSum}
|
|
5754
|
+
got: ${actualSum}`);
|
|
5755
|
+
}
|
|
5756
|
+
log("checksum OK");
|
|
5757
|
+
const tmpExtract = join3(tmpdir(), `fbi-proxy-extract.${process.pid}`);
|
|
5758
|
+
await rm(tmpExtract, { recursive: true, force: true });
|
|
5759
|
+
await mkdir3(tmpExtract, { recursive: true });
|
|
5760
|
+
const tarCmd = platform.ext === "tar.gz" ? ["tar", "-xzf", tmpArchive, "-C", tmpExtract] : ["tar", "-xf", tmpArchive, "-C", tmpExtract];
|
|
5761
|
+
log(`extracting: ${tarCmd.join(" ")}`);
|
|
5762
|
+
const proc = Bun.spawn(tarCmd, { stdout: "inherit", stderr: "inherit" });
|
|
5763
|
+
const code = await proc.exited;
|
|
5764
|
+
if (code !== 0) {
|
|
5765
|
+
await rm(tmpArchive, { force: true });
|
|
5766
|
+
await rm(tmpExtract, { recursive: true, force: true });
|
|
5767
|
+
throw new Error(`tar extraction exited with code ${code}`);
|
|
5768
|
+
}
|
|
5769
|
+
const extractedBinary = join3(tmpExtract, binaryName);
|
|
5770
|
+
if (!existsSync3(extractedBinary)) {
|
|
5771
|
+
await rm(tmpArchive, { force: true });
|
|
5772
|
+
await rm(tmpExtract, { recursive: true, force: true });
|
|
5773
|
+
throw new Error(`archive did not contain expected '${binaryName}' at top level`);
|
|
5774
|
+
}
|
|
5775
|
+
try {
|
|
5776
|
+
await rename(extractedBinary, destPath);
|
|
5777
|
+
} catch (err) {
|
|
5778
|
+
if (err.code === "EXDEV") {
|
|
5779
|
+
await copyFile(extractedBinary, destPath);
|
|
5780
|
+
} else {
|
|
5781
|
+
throw err;
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
if (platform.os !== "windows")
|
|
5785
|
+
await chmod4(destPath, 493);
|
|
5786
|
+
await rm(tmpArchive, { force: true });
|
|
5787
|
+
await rm(tmpExtract, { recursive: true, force: true });
|
|
5788
|
+
log(`installed: ${destPath}`);
|
|
5789
|
+
return destPath;
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5792
|
+
// ts/auth/spawnCaddy.ts
|
|
5793
|
+
async function resolveCaddyBinary() {
|
|
5794
|
+
const fromEnv = process.env.CADDY_BIN;
|
|
5795
|
+
if (fromEnv && await isExecutable(fromEnv))
|
|
5796
|
+
return fromEnv;
|
|
5797
|
+
const fromPath = await whichCaddy();
|
|
5798
|
+
if (fromPath)
|
|
5799
|
+
return fromPath;
|
|
5800
|
+
const downloaded = join4(homedir4(), ".fbi-proxy", "bin", "caddy");
|
|
5801
|
+
if (existsSync4(downloaded) && await isExecutable(downloaded)) {
|
|
5802
|
+
return downloaded;
|
|
5803
|
+
}
|
|
5804
|
+
if (process.env.FBI_CADDY_AUTO_DOWNLOAD === "false") {
|
|
5805
|
+
return null;
|
|
5806
|
+
}
|
|
5807
|
+
try {
|
|
5808
|
+
console.log("[caddy] no binary found — downloading the latest release from GitHub (~30 MB).");
|
|
5809
|
+
console.log("[caddy] (set FBI_CADDY_AUTO_DOWNLOAD=false to opt out)");
|
|
5810
|
+
const path3 = await downloadCaddy({
|
|
5811
|
+
log: (m) => console.log(`[caddy-download] ${m}`)
|
|
5812
|
+
});
|
|
5813
|
+
return path3;
|
|
5814
|
+
} catch (err) {
|
|
5815
|
+
console.error(`[caddy] auto-download failed: ${err.message}`);
|
|
5816
|
+
return null;
|
|
5817
|
+
}
|
|
5818
|
+
}
|
|
5819
|
+
function caddyNotFoundMessage() {
|
|
5820
|
+
return [
|
|
5821
|
+
"",
|
|
5822
|
+
"[fbi-proxy] --with-caddy was passed but Caddy could not be found or downloaded.",
|
|
5823
|
+
"",
|
|
5824
|
+
"Auto-download from GitHub Releases is the default — if you saw a",
|
|
5825
|
+
"download error above, check your network or set FBI_CADDY_AUTO_DOWNLOAD=false",
|
|
5826
|
+
"and install Caddy manually:",
|
|
5827
|
+
"",
|
|
5828
|
+
" - macOS: brew install caddy",
|
|
5829
|
+
" - Debian: sudo apt install caddy (or see https://caddyserver.com/docs/install)",
|
|
5830
|
+
" - Windows: scoop install caddy (or: winget install CaddyServer.Caddy)",
|
|
5831
|
+
" - Manual: https://caddyserver.com/download",
|
|
5832
|
+
"",
|
|
5833
|
+
"Or point fbi-proxy at an existing binary:",
|
|
5834
|
+
" CADDY_BIN=/path/to/caddy bunx fbi-proxy --with-caddy --domain <your-domain>",
|
|
5835
|
+
""
|
|
5836
|
+
].join(`
|
|
5837
|
+
`);
|
|
5838
|
+
}
|
|
5839
|
+
async function spawnCaddy(opts) {
|
|
5840
|
+
const binary = opts.binary ?? await resolveCaddyBinary();
|
|
5841
|
+
if (!binary)
|
|
5842
|
+
return null;
|
|
5843
|
+
console.log(`[caddy] using binary: ${binary}`);
|
|
5844
|
+
console.log(`[caddy] config: ${opts.caddyfilePath}`);
|
|
5845
|
+
const proc = $`${binary} run --config ${opts.caddyfilePath} --adapter caddyfile`.process;
|
|
5846
|
+
proc.on("exit", (code) => {
|
|
5847
|
+
console.log(`[caddy] exited with code ${code}`);
|
|
5848
|
+
});
|
|
5849
|
+
return {
|
|
5850
|
+
pid: proc.pid,
|
|
5851
|
+
caddyfilePath: opts.caddyfilePath,
|
|
5852
|
+
binary,
|
|
5853
|
+
kill: () => proc.kill?.()
|
|
5854
|
+
};
|
|
5855
|
+
}
|
|
5856
|
+
async function isExecutable(path3) {
|
|
5857
|
+
try {
|
|
5858
|
+
await access(path3, fsConstants.X_OK);
|
|
5859
|
+
return true;
|
|
5860
|
+
} catch {
|
|
5861
|
+
return false;
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
async function whichCaddy() {
|
|
5865
|
+
const result = await $`which caddy`.catch(() => null);
|
|
5866
|
+
if (!result || result.code !== 0)
|
|
5867
|
+
return null;
|
|
5868
|
+
const out = result.out.trim();
|
|
5869
|
+
return out.length > 0 ? out.split(`
|
|
5870
|
+
`)[0].trim() : null;
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5241
5873
|
// ts/cli.ts
|
|
5242
5874
|
var originalCwd = process.cwd();
|
|
5243
|
-
process.chdir(
|
|
5244
|
-
await yargs_default(hideBin(process.argv)).option("dev", {
|
|
5245
|
-
alias: "d",
|
|
5875
|
+
process.chdir(path3.resolve(import.meta.dir, ".."));
|
|
5876
|
+
var argv = await yargs_default(hideBin(process.argv)).option("dev", {
|
|
5246
5877
|
type: "boolean",
|
|
5247
5878
|
default: false,
|
|
5248
5879
|
description: "Run in development mode"
|
|
5880
|
+
}).option("with-auth", {
|
|
5881
|
+
type: "boolean",
|
|
5882
|
+
default: false,
|
|
5883
|
+
description: "Start the fbi-auth gateway alongside the proxy (Phase 1: Google OAuth)"
|
|
5884
|
+
}).option("with-caddy", {
|
|
5885
|
+
type: "boolean",
|
|
5886
|
+
default: false,
|
|
5887
|
+
description: "Auto-generate a Caddyfile and spawn Caddy alongside the proxy"
|
|
5888
|
+
}).option("domain", {
|
|
5889
|
+
type: "string",
|
|
5890
|
+
default: "fbi.com",
|
|
5891
|
+
description: "Domain to gate (default: fbi.com)"
|
|
5892
|
+
}).option("reconfigure", {
|
|
5893
|
+
type: "boolean",
|
|
5894
|
+
default: false,
|
|
5895
|
+
description: "Run the interactive fbi-auth setup wizard to (re)write auth.json (requires a TTY)"
|
|
5896
|
+
}).option("acme-email", {
|
|
5897
|
+
type: "string",
|
|
5898
|
+
description: "Optional ACME account email for the generated Caddyfile (Let's Encrypt notifications)"
|
|
5899
|
+
}).option("tls-mode", {
|
|
5900
|
+
type: "string",
|
|
5901
|
+
choices: ["auto", "internal"],
|
|
5902
|
+
description: "TLS strategy for --with-caddy. 'auto' uses ACME (Let's Encrypt); 'internal' uses Caddy's local CA. Defaults to 'internal' for fbi.com, 'auto' otherwise."
|
|
5249
5903
|
}).help().argv;
|
|
5250
5904
|
console.log("Preparing Binaries");
|
|
5251
5905
|
var FBI_PROXY_PORT = process.env.FBI_PROXY_PORT || String(await getPorts({ port: 2432 }));
|
|
@@ -5264,11 +5918,31 @@ var proxyProcess = await hotMemo(async () => {
|
|
|
5264
5918
|
});
|
|
5265
5919
|
return p;
|
|
5266
5920
|
});
|
|
5267
|
-
console.log("All services started successfully!");
|
|
5268
5921
|
console.log(`Proxy server PID: ${proxyProcess.pid}`);
|
|
5269
5922
|
console.log(`Proxy server running on port: ${FBI_PROXY_PORT}`);
|
|
5923
|
+
var authHandle;
|
|
5924
|
+
if (argv["with-auth"]) {
|
|
5925
|
+
authHandle = await startFbiAuth({
|
|
5926
|
+
domain: argv.domain,
|
|
5927
|
+
reconfigure: argv.reconfigure
|
|
5928
|
+
});
|
|
5929
|
+
}
|
|
5930
|
+
var caddyHandle;
|
|
5931
|
+
if (argv["with-caddy"]) {
|
|
5932
|
+
caddyHandle = await startCaddy({
|
|
5933
|
+
domain: argv.domain,
|
|
5934
|
+
fbiProxyPort: Number(FBI_PROXY_PORT),
|
|
5935
|
+
fbiAuthPort: authHandle?.port,
|
|
5936
|
+
withAuth: Boolean(argv["with-auth"]),
|
|
5937
|
+
acmeEmail: argv["acme-email"],
|
|
5938
|
+
tlsMode: argv["tls-mode"] ?? undefined
|
|
5939
|
+
}) ?? undefined;
|
|
5940
|
+
}
|
|
5941
|
+
console.log("All services started successfully!");
|
|
5270
5942
|
var exit = () => {
|
|
5271
5943
|
console.log("Shutting down...");
|
|
5944
|
+
caddyHandle?.kill();
|
|
5945
|
+
authHandle?.kill();
|
|
5272
5946
|
proxyProcess?.kill?.();
|
|
5273
5947
|
process.exit(0);
|
|
5274
5948
|
};
|
|
@@ -5278,3 +5952,72 @@ process.on("uncaughtException", (err) => {
|
|
|
5278
5952
|
console.error("Uncaught exception:", err);
|
|
5279
5953
|
exit();
|
|
5280
5954
|
});
|
|
5955
|
+
async function startFbiAuth(opts) {
|
|
5956
|
+
const configPath = defaultConfigPath();
|
|
5957
|
+
let cfg = await readConfigOrNull(configPath);
|
|
5958
|
+
if (opts.reconfigure) {
|
|
5959
|
+
if (!isTty()) {
|
|
5960
|
+
console.error("[fbi-auth] --reconfigure requires a TTY (interactive terminal).");
|
|
5961
|
+
return;
|
|
5962
|
+
}
|
|
5963
|
+
const prompter = readlinePrompter();
|
|
5964
|
+
cfg = await runWizard(prompter, { domain: opts.domain, existing: cfg });
|
|
5965
|
+
console.log(`[fbi-auth] writing config from wizard \u2192 ${configPath}`);
|
|
5966
|
+
await writeConfig(cfg, configPath);
|
|
5967
|
+
} else if (!cfg) {
|
|
5968
|
+
if (isTty()) {
|
|
5969
|
+
const prompter = readlinePrompter();
|
|
5970
|
+
cfg = await runWizard(prompter, { domain: opts.domain, existing: null });
|
|
5971
|
+
console.log(`[fbi-auth] writing config from wizard \u2192 ${configPath}`);
|
|
5972
|
+
await writeConfig(cfg, configPath);
|
|
5973
|
+
} else {
|
|
5974
|
+
const fromEnv = configFromEnv(opts.domain);
|
|
5975
|
+
if (fromEnv) {
|
|
5976
|
+
console.log(`[fbi-auth] writing config from env vars \u2192 ${configPath}`);
|
|
5977
|
+
await writeConfig(fromEnv, configPath);
|
|
5978
|
+
cfg = fromEnv;
|
|
5979
|
+
} else {
|
|
5980
|
+
console.error(helpfulSetupMessage(opts.domain, configPath));
|
|
5981
|
+
console.error("[fbi-auth] not started \u2014 --with-auth requires a config, env vars, or a TTY for the wizard.");
|
|
5982
|
+
return;
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5986
|
+
console.log(`[fbi-auth] starting (domain=${cfg.domain}, provider=${cfg.provider})`);
|
|
5987
|
+
const handle = await spawnFbiAuth({ configPath });
|
|
5988
|
+
console.log(`[fbi-auth] PID ${handle.pid} listening on 127.0.0.1:${handle.port}`);
|
|
5989
|
+
return handle;
|
|
5990
|
+
}
|
|
5991
|
+
async function startCaddy(opts) {
|
|
5992
|
+
const binary = await resolveCaddyBinary();
|
|
5993
|
+
if (!binary) {
|
|
5994
|
+
console.error(caddyNotFoundMessage());
|
|
5995
|
+
return null;
|
|
5996
|
+
}
|
|
5997
|
+
if (opts.withAuth && opts.fbiAuthPort === undefined) {
|
|
5998
|
+
console.error("[caddy] --with-auth was requested but fbi-auth failed to start; refusing to spawn Caddy with a broken forward_auth target.");
|
|
5999
|
+
return null;
|
|
6000
|
+
}
|
|
6001
|
+
const domain = opts.domain.startsWith(".") ? opts.domain.slice(1) : opts.domain;
|
|
6002
|
+
const tlsMode = opts.tlsMode ?? (domain === "fbi.com" ? "internal" : "auto");
|
|
6003
|
+
const caddyOpts = {
|
|
6004
|
+
domain,
|
|
6005
|
+
fbiProxyPort: opts.fbiProxyPort,
|
|
6006
|
+
tlsMode,
|
|
6007
|
+
acmeEmail: opts.acmeEmail,
|
|
6008
|
+
withAuth: opts.withAuth,
|
|
6009
|
+
...opts.withAuth && opts.fbiAuthPort !== undefined ? {
|
|
6010
|
+
ssoHost: `sso.${domain}`,
|
|
6011
|
+
fbiAuthPort: opts.fbiAuthPort
|
|
6012
|
+
} : {}
|
|
6013
|
+
};
|
|
6014
|
+
const caddyfilePath = defaultCaddyfilePath();
|
|
6015
|
+
const { path: writtenPath } = await writeCaddyfile(caddyOpts, caddyfilePath);
|
|
6016
|
+
console.log(`[caddy] wrote Caddyfile \u2192 ${writtenPath}`);
|
|
6017
|
+
console.log(`[caddy] domain=${domain} tlsMode=${tlsMode} withAuth=${opts.withAuth}`);
|
|
6018
|
+
const handle = await spawnCaddy({ caddyfilePath: writtenPath, binary });
|
|
6019
|
+
if (handle) {
|
|
6020
|
+
console.log(`[caddy] PID ${handle.pid}`);
|
|
6021
|
+
}
|
|
6022
|
+
return handle;
|
|
6023
|
+
}
|