@nodecg/types
Version:
Dynamic broadcast graphics rendered in a browser
1,199 lines (1,177 loc) • 166 kB
JavaScript
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) {
__defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
}
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let __nodecg_internal_util = require("@nodecg/internal-util");
let __effect_platform_node_NodeRuntime = require("@effect/platform-node/NodeRuntime");
__effect_platform_node_NodeRuntime = __toESM(__effect_platform_node_NodeRuntime);
let effect = require("effect");
let node_fs = require("node:fs");
node_fs = __toESM(node_fs);
let node_path = require("node:path");
node_path = __toESM(node_path);
let yargs = require("yargs");
let cosmiconfig = require("cosmiconfig");
let klona_json = require("klona/json");
let zod = require("zod");
let node_os = require("node:os");
node_os = __toESM(node_os);
let __sentry_node = require("@sentry/node");
__sentry_node = __toESM(__sentry_node);
let express = require("express");
express = __toESM(express);
let os = require("os");
os = __toESM(os);
let node_crypto = require("node:crypto");
node_crypto = __toESM(node_crypto);
let cookie_parser = require("cookie-parser");
cookie_parser = __toESM(cookie_parser);
let express_session = require("express-session");
express_session = __toESM(express_session);
let passport = require("passport");
passport = __toESM(passport);
let passport_discord = require("passport-discord");
let passport_local = require("passport-local");
let passport_steam = require("passport-steam");
passport_steam = __toESM(passport_steam);
let passport_twitch_helix = require("passport-twitch-helix");
let node_util = require("node:util");
let winston = require("winston");
winston = __toESM(winston);
let __nodecg_database_adapter_sqlite_legacy = require("@nodecg/database-adapter-sqlite-legacy");
let body_parser = require("body-parser");
body_parser = __toESM(body_parser);
let compression = require("compression");
compression = __toESM(compression);
let express_transform_bare_module_specifiers = require("express-transform-bare-module-specifiers");
express_transform_bare_module_specifiers = __toESM(express_transform_bare_module_specifiers);
let fast_memoize = require("fast-memoize");
fast_memoize = __toESM(fast_memoize);
let lodash = require("lodash");
let socket_io = require("socket.io");
socket_io = __toESM(socket_io);
let __nodecg_json_schema_lib = require("@nodecg/json-schema-lib");
__nodecg_json_schema_lib = __toESM(__nodecg_json_schema_lib);
let hasha = require("hasha");
hasha = __toESM(hasha);
let object_path = require("object-path");
object_path = __toESM(object_path);
let events = require("events");
let __nodecg_json_schema_defaults = require("@nodecg/json-schema-defaults");
__nodecg_json_schema_defaults = __toESM(__nodecg_json_schema_defaults);
let ajv = require("ajv");
ajv = __toESM(ajv);
let ajv_dist_2019 = require("ajv/dist/2019");
ajv_dist_2019 = __toESM(ajv_dist_2019);
let ajv_dist_2020 = require("ajv/dist/2020");
ajv_dist_2020 = __toESM(ajv_dist_2020);
let ajv_draft_04 = require("ajv-draft-04");
ajv_draft_04 = __toESM(ajv_draft_04);
let ajv_formats = require("ajv-formats");
ajv_formats = __toESM(ajv_formats);
let fp_ts_Either = require("fp-ts/Either");
fp_ts_Either = __toESM(fp_ts_Either);
let json_ptr = require("json-ptr");
let multer = require("multer");
multer = __toESM(multer);
let chokidar = require("chokidar");
chokidar = __toESM(chokidar);
let path = require("path");
path = __toESM(path);
let semver = require("semver");
semver = __toESM(semver);
let fp_ts_function = require("fp-ts/function");
let fp_ts_IOEither = require("fp-ts/IOEither");
fp_ts_IOEither = __toESM(fp_ts_IOEither);
let fp_ts_Option = require("fp-ts/Option");
fp_ts_Option = __toESM(fp_ts_Option);
let extend = require("extend");
extend = __toESM(extend);
let fp_ts_boolean = require("fp-ts/boolean");
fp_ts_boolean = __toESM(fp_ts_boolean);
let fp_ts_IO = require("fp-ts/IO");
fp_ts_IO = __toESM(fp_ts_IO);
let fs = require("fs");
fs = __toESM(fs);
let git_rev_sync = require("git-rev-sync");
git_rev_sync = __toESM(git_rev_sync);
let cheerio = require("cheerio");
cheerio = __toESM(cheerio);
let is_error = require("is-error");
is_error = __toESM(is_error);
let serialize_error = require("serialize-error");
//#region src/server/_effect/boundary.ts
var UnknownError = class extends effect.Data.TaggedError("UnknownError") {
constructor(cause) {
super();
this.cause = cause;
this.message = this.cause instanceof Error ? this.cause.message : "An unknown error occurred";
}
};
//#endregion
//#region src/server/_effect/expect-error.ts
const expectError = () => effect.Function.identity;
//#endregion
//#region src/server/_effect/log-level.ts
const withLogLevelConfig = effect.Effect.fn(function* (effect$1) {
const logLevel = yield* effect.Config.logLevel("LOG_LEVEL").pipe(effect.Config.withDefault(effect.LogLevel.Info));
return yield* effect.Logger.withMinimumLogLevel(effect$1, logLevel);
});
//#endregion
//#region src/server/_effect/span-logger.ts
const withSpanProcessorLive = effect.Effect.fn(function* (effect$1) {
if (!(yield* effect.Config.boolean("LOG_SPAN").pipe(effect.Config.withDefault(false)))) return yield* effect$1;
const NodeSdk = yield* effect.Effect.promise(() => import("@effect/opentelemetry/NodeSdk"));
const { SpanStatusCode } = yield* effect.Effect.promise(() => Promise.resolve().then(() => require("./esm-D8HCo8cr.js")));
const runtime = yield* effect.Effect.runtime();
const layer = NodeSdk.layer(() => ({
resource: { serviceName: "nodecg" },
spanProcessor: {
forceFlush: () => Promise.resolve(),
onStart: (span) => {
effect.Runtime.runSync(runtime, effect.Effect.logTrace(`▶️ ${span.name}`));
},
onEnd: (span) => {
const formattedDuration = (0, effect.pipe)(span.duration, effect.Duration.toMillis, effect.Number.round(0), effect.Duration.millis, effect.Duration.format, effect.String.split(" "), effect.Array.take(2), effect.Array.join(" "));
let log$7 = `${span.status.code === SpanStatusCode.ERROR ? "❌" : "✅"} ${span.name} (${formattedDuration})`;
if (span.status.code === SpanStatusCode.ERROR) log$7 += ` ${span.status.message}`;
effect.Runtime.runSync(runtime, effect.Effect.logTrace(log$7));
},
shutdown: () => Promise.resolve()
}
}));
return yield* effect.Effect.provide(effect$1, layer);
});
//#endregion
//#region src/types/logger-interface.ts
const LogLevels = [
"verbose",
"debug",
"info",
"warn",
"error",
"silent"
];
const LogLevel = {
Trace: "verbose",
Debug: "debug",
Info: "info",
Warn: "warn",
Error: "error",
Silent: "silent"
};
//#endregion
//#region src/types/nodecg-config-schema.ts
const parsedArgv = zod.z.object({
bundlesEnabled: zod.z.string().optional().transform((val) => val?.split(",")),
bundlesDisabled: zod.z.string().optional().transform((val) => val?.split(",")),
bundlesPaths: zod.z.string().optional().transform((val) => val?.split(","))
}).parse(yargs.argv);
const nodecgConfigSchema = zod.z.object({
host: zod.z.string().default("0.0.0.0").describe("The IP address or hostname that NodeCG should bind to."),
port: zod.z.number().int().positive().default(9090).describe("The port that NodeCG should listen on."),
baseURL: zod.z.string().optional().describe("The URL of this instance. Used for things like cookies. Defaults to HOST:PORT. If you use a reverse proxy, you'll likely need to set this value."),
exitOnUncaught: zod.z.boolean().default(true).describe("Whether or not to exit on uncaught exceptions."),
logging: zod.z.object({
console: zod.z.object({
enabled: zod.z.boolean().default(true).describe("Whether to enable console logging."),
level: zod.z.enum(LogLevels).default("info").describe("The log level to use."),
timestamps: zod.z.boolean().default(true).describe("Whether to add timestamps to the console logging."),
replicants: zod.z.boolean().default(false).describe("Whether to enable logging of the Replicants subsystem. Very spammy.")
}).default({ enabled: true }),
file: zod.z.object({
enabled: zod.z.boolean().default(false).describe("Whether to enable file logging."),
level: zod.z.enum(LogLevels).default("info").describe("The log level to use."),
path: zod.z.string().default("logs/nodecg.log").describe("The filepath to log to."),
timestamps: zod.z.boolean().default(true).describe("Whether to add timestamps to the file logging."),
replicants: zod.z.boolean().default(false).describe("Whether to enable logging of the Replicants subsystem. Very spammy.")
}).default({ enabled: false })
}).default({ console: {} }),
bundles: zod.z.object({
enabled: zod.z.array(zod.z.string()).nullable().default(parsedArgv.bundlesEnabled ?? null).describe("A whitelist array of bundle names."),
disabled: zod.z.array(zod.z.string()).nullable().default(parsedArgv.bundlesDisabled ?? null).describe("A blacklist array of bundle names."),
paths: zod.z.array(zod.z.string()).default(parsedArgv.bundlesPaths ?? []).describe("An array of additional paths where bundles are located.")
}).default({
enabled: null,
disabled: null,
paths: []
}),
login: zod.z.object({
enabled: zod.z.boolean().default(false).describe("Whether to enable login security."),
sessionSecret: zod.z.string().optional().describe("The secret used to salt sessions."),
forceHttpsReturn: zod.z.boolean().default(false).describe("Forces Steam & Twitch login return URLs to use HTTPS instead of HTTP. Useful in reverse proxy setups."),
steam: zod.z.object({
enabled: zod.z.boolean().default(false).describe("Whether to enable Steam authentication."),
apiKey: zod.z.string().optional().describe("A Steam API Key. Obtained from http://steamcommunity.com/dev/apikey"),
allowedIds: zod.z.array(zod.z.string()).default([]).describe("Which 64 bit Steam IDs to allow. Can be obtained from https://steamid.io/")
}).optional().refine((val) => val?.enabled ? typeof val.apiKey === "string" : true, { message: "\"login.steam.apiKey\" must be a string" }),
twitch: zod.z.object({
enabled: zod.z.boolean().default(false).describe("Whether to enable Twitch authentication."),
clientID: zod.z.string().optional().describe("A Twitch application ClientID http://twitch.tv/kraken/oauth2/clients/new"),
clientSecret: zod.z.string().optional().describe("A Twitch application ClientSecret http://twitch.tv/kraken/oauth2/clients/new"),
scope: zod.z.string().default("user_read").describe("A space-separated string of Twitch application permissions."),
allowedUsernames: zod.z.array(zod.z.string()).default([]).describe("Which Twitch usernames to allow."),
allowedIds: zod.z.array(zod.z.string()).default([]).describe("Which Twitch IDs to allow. Can be obtained from https://twitchinsights.net/checkuser")
}).optional().refine((val) => val?.enabled ? typeof val.clientID === "string" : true, { message: "\"login.twitch.clientID\" must be a string" }).refine((val) => val?.enabled ? typeof val.clientSecret === "string" : true, { message: "\"login.twitch.clientID\" must be a string" }),
discord: zod.z.object({
enabled: zod.z.boolean().default(false).describe("Whether to enable Discord authentication."),
clientID: zod.z.string().optional().describe("A Discord application ClientID https://discord.com/developers/applications"),
clientSecret: zod.z.string().optional().describe("A Discord application ClientSecret https://discord.com/developers/applications"),
scope: zod.z.string().default("identify").describe("A space-separated string of Discord application scopes. https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes"),
allowedUserIDs: zod.z.array(zod.z.string()).default([]).describe("Which Discord user IDs to allow."),
allowedGuilds: zod.z.array(zod.z.object({
guildID: zod.z.string().describe("Users in this Discord Server are allowed to log in."),
allowedRoleIDs: zod.z.array(zod.z.string()).default([]).describe("Additionally require one of the roles on the server to log in."),
guildBotToken: zod.z.string().default("").describe("Discord bot token, needed if allowedRoleIDs is used.")
})).default([])
}).optional().refine((val) => val?.enabled ? typeof val.clientID === "string" : true, { message: "\"login.discord.clientID\" must be a string" }).refine((val) => val?.enabled ? typeof val.clientSecret === "string" : true, { message: "\"login.discord.clientSecret\" must be a string" }),
local: zod.z.object({
enabled: zod.z.boolean().default(false).describe("Enable Local authentication."),
allowedUsers: zod.z.array(zod.z.object({
username: zod.z.string(),
password: zod.z.string()
})).default([]).describe("Which users can log in.")
}).optional()
}).refine((val) => val.enabled ? typeof val.sessionSecret === "string" : true, { message: "\"login.sessionSecret\" must be a string" }).default({ enabled: false }),
ssl: zod.z.object({
enabled: zod.z.boolean().default(false).describe("Whether to enable SSL/HTTPS encryption."),
allowHTTP: zod.z.boolean().default(false).describe("Whether to allow insecure HTTP connections while SSL is active."),
keyPath: zod.z.string().optional().describe("The path to an SSL key file."),
certificatePath: zod.z.string().optional().describe("The path to an SSL certificate file."),
passphrase: zod.z.string().optional().describe("The passphrase for the provided key file.")
}).default({ enabled: false }).refine((val) => val.enabled ? typeof val.keyPath === "string" : true, { message: "\"ssl.keyPath\" must be a string" }).refine((val) => val.enabled ? typeof val.certificatePath === "string" : true, { message: "\"ssl.certificatePath\" must be a string" }),
sentry: zod.z.object({
enabled: zod.z.boolean().default(true).describe("Whether to enable Sentry error reporting."),
dsn: zod.z.string().optional().describe("Your project's DSN, used to route alerts to the correct place.")
}).default({ enabled: false }).refine((val) => val.enabled ? typeof val.dsn === "string" : true, { message: "\"sentry.dsn\" must be a string" })
}).transform((val) => {
const host = val.host === "0.0.0.0" ? "localhost" : val.host;
return {
...val,
baseURL: val.baseURL ?? `${host}:${val.port}`
};
});
//#endregion
//#region src/server/config/loader.ts
const loadConfig = (cfgDirOrFile) => {
let isFile = false;
try {
isFile = node_fs.lstatSync(cfgDirOrFile).isFile();
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
const cfgDir = isFile ? node_path.dirname(cfgDirOrFile) : cfgDirOrFile;
const userCfg = (0, cosmiconfig.cosmiconfigSync)("nodecg", {
searchPlaces: isFile ? [node_path.basename(cfgDirOrFile)] : [
"nodecg.json",
"nodecg.yaml",
"nodecg.yml",
"nodecg.js",
"nodecg.config.js"
],
stopDir: cfgDir
}).search(cfgDir)?.config ?? {};
if (userCfg?.bundles?.enabled && userCfg?.bundles?.disabled) throw new Error("nodecg.json may only contain EITHER bundles.enabled OR bundles.disabled, not both.");
else if (!userCfg) console.info("[nodecg] No config found, using defaults.");
const parseResult = nodecgConfigSchema.safeParse(userCfg);
if (!parseResult.success) {
console.error("[nodecg] Config invalid:", parseResult.error.errors[0]?.message);
throw new Error(parseResult.error.errors[0]?.message);
}
const config$1 = parseResult.data;
const filteredConfig$1 = {
host: config$1.host,
port: config$1.port,
baseURL: config$1.baseURL,
logging: {
console: {
enabled: config$1.logging.console.enabled,
level: config$1.logging.console.level,
timestamps: config$1.logging.console.timestamps,
replicants: config$1.logging.console.replicants
},
file: {
enabled: config$1.logging.file.enabled,
level: config$1.logging.file.level,
timestamps: config$1.logging.file.timestamps,
replicants: config$1.logging.file.replicants
}
},
login: { enabled: config$1.login.enabled },
sentry: {
enabled: config$1.sentry.enabled,
dsn: config$1.sentry.enabled ? config$1.sentry.dsn : void 0
}
};
if (config$1.login.enabled && config$1.login.steam) filteredConfig$1.login.steam = { enabled: config$1.login.steam.enabled };
if (config$1.login.enabled && config$1.login.twitch) filteredConfig$1.login.twitch = {
enabled: config$1.login.twitch.enabled,
clientID: config$1.login.twitch.enabled ? config$1.login.twitch.clientID : void 0,
scope: config$1.login.twitch.enabled ? config$1.login.twitch.scope : void 0
};
if (config$1.login.enabled && config$1.login.local) filteredConfig$1.login.local = { enabled: config$1.login.local.enabled };
if (config$1.login.enabled && config$1.login.discord) filteredConfig$1.login.discord = {
enabled: config$1.login.discord.enabled,
clientID: config$1.login.discord.enabled ? config$1.login.discord.clientID : void 0,
scope: config$1.login.discord.enabled ? config$1.login.discord.scope : void 0
};
if (config$1.ssl) filteredConfig$1.ssl = { enabled: config$1.ssl.enabled };
return {
config: (0, klona_json.klona)(config$1),
filteredConfig: (0, klona_json.klona)(filteredConfig$1)
};
};
//#endregion
//#region src/server/config/index.ts
const cfgDirectoryPath = yargs.argv.cfgPath ?? node_path.join(__nodecg_internal_util.rootPaths.getRuntimeRoot(), "cfg");
if (!node_fs.existsSync(cfgDirectoryPath)) node_fs.mkdirSync(cfgDirectoryPath, { recursive: true });
const { config, filteredConfig } = loadConfig(cfgDirectoryPath);
const exitOnUncaught = config.exitOnUncaught;
const sentryEnabled = config.sentry?.enabled;
//#endregion
//#region src/server/_effect/event-listener.ts
const waitForEvent = (eventEmitter, eventName) => effect.Effect.async((resume) => {
const handler = (...payload) => {
resume(effect.Effect.succeed(payload));
};
eventEmitter.once(eventName, handler);
return effect.Effect.sync(() => {
eventEmitter.removeListener(eventName, handler);
});
});
const listenToEvent = (eventEmitter, eventName, boundary) => effect.Effect.gen(function* () {
const queue = yield* effect.Match.value(boundary).pipe(effect.Match.when(effect.Predicate.isNumber, (b) => effect.Queue.bounded(b)), effect.Match.when(effect.Predicate.isUndefined, () => effect.Queue.unbounded()), effect.Match.exhaustive);
const handler = (...payload) => {
effect.Queue.unsafeOffer(queue, payload);
};
eventEmitter.addListener(eventName, handler);
yield* effect.Effect.addFinalizer(() => effect.Effect.sync(() => {
eventEmitter.removeListener(eventName, handler);
}));
return effect.Stream.fromQueue(queue);
});
//#endregion
//#region src/server/util/authcheck.ts
/**
* Express middleware that checks if the user is authenticated.
*/
const authCheck = async (req, res, next) => {
try {
if (!config.login?.enabled) {
next();
return;
}
let { user } = req;
let isUsingKeyOrSocketToken = false;
let keyOrSocketTokenAuthenticated = false;
if (req.query.key ?? req.cookies.socketToken) {
isUsingKeyOrSocketToken = true;
const apiKey = await res.locals.databaseAdapter.findApiKey(req.query.key ?? req.cookies.socketToken);
if (!apiKey) {
req.session?.destroy(() => {
res.clearCookie("socketToken", {
secure: req.secure,
sameSite: req.secure ? "none" : void 0
});
res.clearCookie("connect.sid", { path: "/" });
res.clearCookie("io", { path: "/" });
res.redirect("/login");
});
return;
}
user = await res.locals.databaseAdapter.findUser(apiKey.user.id) ?? void 0;
}
if (!user) {
if (req.session) req.session.returnTo = req.url;
res.status(403).redirect("/login");
return;
}
const allowed = res.locals.databaseAdapter.isSuperUser(user);
keyOrSocketTokenAuthenticated = isUsingKeyOrSocketToken && allowed;
const provider = user.identities[0].provider_type;
const providerAllowed = config.login?.[provider]?.enabled;
if ((keyOrSocketTokenAuthenticated || req.isAuthenticated()) && allowed && providerAllowed) {
let apiKey = user.apiKeys[0];
if (!apiKey) {
apiKey = await res.locals.databaseAdapter.createApiKey();
user.apiKeys.push(apiKey);
await res.locals.databaseAdapter.saveUser(user);
}
res.cookie("socketToken", apiKey.secret_key, {
secure: req.secure,
sameSite: req.secure ? "none" : void 0
});
next();
return;
}
if (req.session) req.session.returnTo = req.url;
res.status(403).redirect("/login");
return;
} catch (error) {
next(error);
}
};
//#endregion
//#region src/server/util/nodecg-package-json.ts
function recursivelyFindPackageJson(dir = __dirname) {
const packageJsonPath = node_path.default.join(dir, "package.json");
if (node_fs.default.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(node_fs.default.readFileSync(packageJsonPath, "utf-8"));
if (packageJson.name === "nodecg") return packageJson;
}
const parentDir = node_path.default.dirname(dir);
if (dir === parentDir) throw new Error("Could not find NodeCG root path");
return recursivelyFindPackageJson(parentDir);
}
const nodecgPackageJson = recursivelyFindPackageJson(__dirname);
//#endregion
//#region src/server/server/sentry-config.ts
const baseSentryConfig = {
dsn: config.sentry.enabled ? config.sentry.dsn : "",
serverName: node_os.hostname(),
version: nodecgPackageJson.version
};
const sentryConfigRouter = effect.Effect.fn("sentryConfigRouter")(function* (bundleManager) {
const bundleMetadata = [];
const app = (0, express.default)();
yield* effect.Effect.forkScoped(waitForEvent(bundleManager, "ready").pipe(effect.Effect.andThen(() => effect.Effect.sync(() => {
__sentry_node.configureScope((scope) => {
bundleManager.all().forEach((bundle) => {
bundleMetadata.push({
name: bundle.name,
git: bundle.git,
version: bundle.version
});
});
scope.setExtra("bundles", bundleMetadata);
});
}))));
const gitChangedStream = yield* listenToEvent(bundleManager, "gitChanged");
yield* effect.Effect.forkScoped(gitChangedStream.pipe(effect.Stream.runForEach(([bundle]) => effect.Effect.sync(() => {
const metadataToUpdate = bundleMetadata.find((data) => data.name === bundle.name);
if (!metadataToUpdate) return;
metadataToUpdate.git = bundle.git;
metadataToUpdate.version = bundle.version;
}))));
app.get("/sentry.js", authCheck, (_req, res) => {
res.type(".js");
res.render(node_path.join(__nodecg_internal_util.rootPaths.nodecgInstalledPath, "dist/server/templates/sentry.js.tmpl"), {
baseSentryConfig,
bundleMetadata
});
});
return app;
});
//#endregion
//#region src/server/logger/logger.server.ts
/**
* A factory that configures and returns a Logger constructor.
*
* @returns A constructor used to create discrete logger instances.
*/
function loggerFactory(initialOpts = {}, sentry = void 0) {
initialOpts = initialOpts || {};
initialOpts.console = initialOpts.console ?? {};
initialOpts.file = initialOpts.file ?? {};
initialOpts.file.path = initialOpts.file.path ?? "logs/nodecg.log";
const consoleTransport = new winston.default.transports.Console({
level: initialOpts.console.level ?? LogLevel.Info,
silent: !initialOpts.console.enabled,
stderrLevels: ["warn", "error"],
format: winston.default.format.combine(winston.default.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.default.format.errors({ stack: true }), winston.default.format.colorize(), winston.default.format.printf((info) => `${initialOpts?.console?.timestamps ? `${info.timestamp} - ` : ""}${info.level}: ${info.message}`))
});
const fileTransport = new winston.default.transports.File({
filename: initialOpts.file.path,
level: initialOpts.file.level ?? LogLevel.Info,
silent: !initialOpts.file.enabled,
format: winston.default.format.combine(winston.default.format.timestamp(), winston.default.format.errors({ stack: true }), winston.default.format.printf((info) => `${initialOpts?.file?.timestamps ? `${info.timestamp} - ` : ""}${info.level}: ${info.message}`))
});
if (typeof initialOpts.file.path !== "undefined") {
fileTransport.filename = initialOpts.file.path;
if (!node_fs.default.existsSync(node_path.dirname(initialOpts.file.path))) node_fs.default.mkdirSync(node_path.dirname(initialOpts.file.path), { recursive: true });
}
winston.default.addColors({
verbose: "green",
debug: "cyan",
info: "white",
warn: "yellow",
error: "red"
});
const consoleLogger = winston.default.createLogger({
transports: [consoleTransport],
levels: {
verbose: 4,
trace: 4,
debug: 3,
info: 2,
warn: 1,
error: 0
}
});
const fileLogger = winston.default.createLogger({
transports: [fileTransport],
levels: {
verbose: 4,
trace: 4,
debug: 3,
info: 2,
warn: 1,
error: 0
}
});
/**
* Constructs a new Logger instance that prefixes all output with the given name.
* @param name {String} - The label to prefix all output of this logger with.
* @returns {Object} - A Logger instance.
* @constructor
*/
return class Logger$2 {
static _consoleLogger = consoleLogger;
static _fileLogger = fileLogger;
static _shouldConsoleLogReplicants = Boolean(initialOpts.console?.replicants);
static _shouldFileLogReplicants = Boolean(initialOpts.file?.replicants);
constructor(name) {
this.name = name;
this.name = name;
}
trace(...args) {
[consoleLogger, fileLogger].forEach((logger$1) => logger$1.verbose(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`));
}
debug(...args) {
[consoleLogger, fileLogger].forEach((logger$1) => logger$1.debug(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`));
}
info(...args) {
[consoleLogger, fileLogger].forEach((logger$1) => logger$1.info(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`));
}
warn(...args) {
[consoleLogger, fileLogger].forEach((logger$1) => logger$1.warn(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`));
}
error(...args) {
[consoleLogger, fileLogger].forEach((logger$1) => logger$1.error(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`));
if (sentry) {
const formattedArgs = args.map((argument) => typeof argument === "object" ? (0, node_util.inspect)(argument, {
depth: null,
showProxy: true
}) : argument);
sentry.captureException(/* @__PURE__ */ new Error(`[${this.name}] ` + (0, node_util.format)(formattedArgs[0], ...formattedArgs.slice(1))));
}
}
replicants(...args) {
if (Logger$2._shouldConsoleLogReplicants) consoleLogger.info(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`);
if (Logger$2._shouldFileLogReplicants) fileLogger.info(`[${this.name}] ${(0, node_util.format)(args[0], ...args.slice(1))}`);
}
};
}
//#endregion
//#region src/server/logger/index.ts
let Logger;
if (config.sentry?.enabled) Logger = loggerFactory(config.logging, __sentry_node);
else Logger = loggerFactory(config.logging);
function createLogger(name) {
return new Logger(name);
}
//#endregion
//#region src/server/server/login/index.ts
const log$6 = createLogger("login");
const protocol = config.ssl?.enabled || config.login.enabled && config.login.forceHttpsReturn ? "https" : "http";
async function makeDiscordAPIRequest(guild, userID) {
const res = await fetch(`https://discord.com/api/v8/guilds/${guild.guildID}/members/${userID}`, { headers: { Authorization: `Bot ${guild.guildBotToken}` } });
const data = await res.json();
if (res.status === 200) return [
guild,
false,
data
];
return [
guild,
true,
data
];
}
function createMiddleware(db, callbacks) {
passport.default.serializeUser((user, done) => {
done(null, user.id);
});
passport.default.deserializeUser(async (id, done) => {
try {
done(null, await db.findUser(id));
} catch (error) {
done(error);
}
});
if (config.login.enabled && config.login.steam?.enabled && config.login.steam.apiKey) {
const steamLoginConfig = config.login.steam;
const apiKey = config.login.steam.apiKey;
passport.default.use(new passport_steam.default({
returnURL: `${protocol}://${config.baseURL}/login/auth/steam`,
realm: `${protocol}://${config.baseURL}/login/auth/steam`,
apiKey
}, async (_, profile, done) => {
try {
const roles = [];
if (steamLoginConfig?.allowedIds?.includes(profile.id)) {
log$6.info("(Steam) Granting \"%s\" (%s) access", profile.id, profile.displayName);
roles.push(await db.getSuperUserRole());
} else log$6.info("(Steam) Denying \"%s\" (%s) access", profile.id, profile.displayName);
done(void 0, await db.upsertUser({
name: profile.displayName,
provider_type: "steam",
provider_hash: profile.id,
roles
}));
return;
} catch (error) {
done(error);
}
}));
}
if (config.login.enabled && config.login.twitch?.enabled) {
const twitchLoginConfig = config.login.twitch;
const scopesArray = twitchLoginConfig.scope.split(" ");
if (!scopesArray.includes("user:read:email")) scopesArray.push("user:read:email");
const concatScopes = scopesArray.join(" ");
passport.default.use(new passport_twitch_helix.Strategy({
clientID: twitchLoginConfig.clientID,
clientSecret: twitchLoginConfig.clientSecret,
callbackURL: `${protocol}://${config.baseURL}/login/auth/twitch`,
scope: concatScopes,
customHeaders: { "Client-ID": twitchLoginConfig.clientID }
}, async (accessToken, refreshToken, profile, done) => {
try {
const roles = [];
if (twitchLoginConfig.allowedUsernames?.includes(profile.username) ?? twitchLoginConfig.allowedIds?.includes(profile.id)) {
log$6.info("(Twitch) Granting %s access", profile.username);
roles.push(await db.getSuperUserRole());
} else log$6.info("(Twitch) Denying %s access", profile.username);
done(void 0, await db.upsertUser({
name: profile.displayName,
provider_type: "twitch",
provider_hash: profile.id,
provider_access_token: accessToken,
provider_refresh_token: refreshToken,
roles
}));
return;
} catch (error) {
done(error);
}
}));
}
if (config.login.enabled && config.login.discord?.enabled) {
const discordLoginConfig = config.login.discord;
const scopeArray = discordLoginConfig.scope.split(" ");
if (!scopeArray.includes("identify")) scopeArray.push("identify");
if (!scopeArray.includes("guilds") && discordLoginConfig.allowedGuilds) scopeArray.push("guilds");
const scope = scopeArray.join(" ");
passport.default.use(new passport_discord.Strategy({
clientID: discordLoginConfig.clientID,
clientSecret: discordLoginConfig.clientSecret,
callbackURL: `${protocol}://${config.baseURL}/login/auth/discord`,
scope
}, async (accessToken, refreshToken, profile, done) => {
if (!discordLoginConfig) {
done(/* @__PURE__ */ new Error("Discord login config was impossibly undefined."));
return;
}
let allowed = false;
if (discordLoginConfig.allowedUserIDs?.includes(profile.id)) allowed = true;
else if (discordLoginConfig.allowedGuilds) {
const intersectingGuilds = discordLoginConfig.allowedGuilds.filter((allowedGuild) => profile.guilds?.some((profileGuild) => profileGuild.id === allowedGuild.guildID));
const guildRequests = [];
for (const intersectingGuild of intersectingGuilds) if (!intersectingGuild.allowedRoleIDs || intersectingGuild.allowedRoleIDs.length === 0) allowed = true;
else guildRequests.push(makeDiscordAPIRequest(intersectingGuild, profile.id));
if (!allowed) {
const guildsData = await Promise.all(guildRequests);
for (const [guildWithRoles, err, memberResponse] of guildsData) {
if (err) {
log$6.warn(`Got error while trying to get guild ${guildWithRoles.guildID} (Make sure you're using the correct bot token and guild id): ${JSON.stringify(memberResponse)}`);
continue;
}
if (guildWithRoles.allowedRoleIDs.filter((allowedRole) => memberResponse.roles.includes(allowedRole)).length > 0) {
allowed = true;
break;
}
}
}
} else allowed = false;
const roles = [];
if (allowed) {
log$6.info("(Discord) Granting %s#%s (%s) access", profile.username, profile.discriminator, profile.id);
roles.push(await db.getSuperUserRole());
} else log$6.info("(Discord) Denying %s#%s (%s) access", profile.username, profile.discriminator, profile.id);
done(void 0, await db.upsertUser({
name: `${profile.username}#${profile.discriminator}`,
provider_type: "discord",
provider_hash: profile.id,
provider_access_token: accessToken,
provider_refresh_token: refreshToken,
roles
}));
}));
}
if (config.login.enabled && config.login.local?.enabled && config.login.sessionSecret) {
const { sessionSecret, local: { allowedUsers } } = config.login;
const hashes = node_crypto.getHashes();
passport.default.use(new passport_local.Strategy({
usernameField: "username",
passwordField: "password",
session: false
}, async (username, password, done) => {
try {
const roles = [];
const foundUser = allowedUsers?.find((u) => u.username === username);
let allowed = false;
if (foundUser) {
const match = /^([^:]+):(.+)$/.exec(foundUser.password ?? "");
let expected = foundUser.password;
let actual = password;
if (match && hashes.includes(match[1])) {
expected = match[2];
actual = node_crypto.createHmac(match[1], sessionSecret).update(actual, "utf8").digest("hex");
}
if (expected === actual) {
allowed = true;
roles.push(await db.getSuperUserRole());
}
}
log$6.info("(Local) %s \"%s\" access", allowed ? "Granting" : "Denying", username);
done(void 0, await db.upsertUser({
name: username,
provider_type: "local",
provider_hash: username,
roles
}));
return;
} catch (error) {
done(error);
}
}));
}
const app = (0, express.default)();
const redirectPostLogin = (req, res) => {
const url = req.session?.returnTo ?? "/dashboard";
delete req.session.returnTo;
res.redirect(url);
app.emit("login", req.user);
if (req.user) callbacks.onLogin(req.user);
};
if (!config.login.enabled || !config.login.sessionSecret) throw new Error("no session secret defined, can't salt sessions, not safe, aborting");
app.use((0, cookie_parser.default)(config.login.sessionSecret));
const sessionMiddleware = (0, express_session.default)({
resave: false,
saveUninitialized: false,
secret: config.login.sessionSecret,
cookie: {
path: "/",
httpOnly: true,
secure: config.ssl?.enabled
}
});
app.use(sessionMiddleware);
app.use(passport.default.initialize());
app.use(passport.default.session());
app.use("/login", express.default.static(node_path.join(__nodecg_internal_util.rootPaths.nodecgInstalledPath, "dist/client/login")));
app.get("/login", (req, res) => {
if (req.user && db.isSuperUser(req.user)) res.redirect("/dashboard");
else res.render(node_path.join(__nodecg_internal_util.rootPaths.nodecgInstalledPath, "dist/server/templates/login.tmpl"), {
user: req.user,
config
});
});
app.get("/authError", (req, res) => {
res.render(node_path.join(__nodecg_internal_util.rootPaths.nodecgInstalledPath, "dist/server/templates/authError.tmpl"), {
message: req.query.message,
code: req.query.code,
viewUrl: req.query.viewUrl
});
});
app.get("/login/steam", passport.default.authenticate("steam"));
app.get("/login/auth/steam", passport.default.authenticate("steam", { failureRedirect: "/login" }), redirectPostLogin);
app.get("/login/twitch", passport.default.authenticate("twitch"));
app.get("/login/auth/twitch", passport.default.authenticate("twitch", { failureRedirect: "/login" }), redirectPostLogin);
app.get("/login/discord", passport.default.authenticate("discord"));
app.get("/login/auth/discord", passport.default.authenticate("discord", { failureRedirect: "/login" }), redirectPostLogin);
app.get("/login/local", passport.default.authenticate("local"));
app.post("/login/local", passport.default.authenticate("local", { failureRedirect: "/login" }), redirectPostLogin);
app.get("/logout", (req, res) => {
app.emit("logout", req.user);
req.session?.destroy(() => {
res.clearCookie("connect.sid", { path: "/" });
res.clearCookie("io", { path: "/" });
res.clearCookie("socketToken", {
secure: req.secure,
sameSite: req.secure ? "none" : void 0
});
res.redirect("/login");
});
if (req.user) callbacks.onLogout(req.user);
});
return {
app,
sessionMiddleware
};
}
//#endregion
//#region src/shared/utils/errors.ts
/**
* Make a string out of an error (or other equivalents),
* including any additional data such as stack trace if available.
* Safe to use on unknown inputs.
*/
function stringifyError(error, noStack = false) {
const o = stringifyErrorInner(error);
if (noStack || !o.stack) return o.message;
return `${o.message}, ${o.stack}`;
}
function stringifyErrorInner(error) {
let message;
let stack;
if (typeof error === "string") message = error;
else if (error === null) message = "null";
else if (error === void 0) message = "undefined";
else if (error && typeof error === "object") if (typeof error.error === "object" && error.error.message) {
message = error.error.message;
stack = error.error.stack;
} else if (error.reason) if (error.reason.message) {
message = error.reason.message;
stack = error.reason.stack || error.reason.reason;
} else {
message = error.reason;
stack = error.stack;
}
else if (error.message) {
message = error.message;
stack = error.stack;
} else if (error.details) message = error.details;
else try {
message = JSON.stringify(error);
} catch (e) {
message = `${error} (stringifyError: ${e})`;
}
else message = `${error}`;
message = `${message}`;
return {
message,
stack
};
}
//#endregion
//#region src/server/util/throttle-name.ts
const timers = /* @__PURE__ */ new Map();
const queued = /* @__PURE__ */ new Set();
/**
* A standard throttle, but uses a string `name` as the key instead of the callback.
*/
function throttleName(name, callback, duration = 500) {
if (timers.get(name)) {
queued.add(name);
return;
}
callback();
timers.set(name, setTimeout(() => {
timers.delete(name);
if (queued.has(name)) {
queued.delete(name);
callback();
}
}, duration));
}
//#endregion
//#region src/shared/typed-emitter.ts
var TypedEmitter = class {
_emitter = new events.EventEmitter();
addListener(eventName, fn) {
this._emitter.addListener(eventName, fn);
}
on(eventName, fn) {
this._emitter.on(eventName, fn);
}
off(eventName, fn) {
this._emitter.off(eventName, fn);
}
removeListener(eventName, fn) {
this._emitter.removeListener(eventName, fn);
}
emit(eventName, ...params) {
this._emitter.emit(eventName, ...params);
}
once(eventName, fn) {
this._emitter.once(eventName, fn);
}
setMaxListeners(max) {
this._emitter.setMaxListeners(max);
}
listenerCount(eventName) {
return this._emitter.listenerCount(eventName);
}
listeners(eventName) {
return this._emitter.listeners(eventName);
}
};
//#endregion
//#region src/shared/utils/compileJsonSchema.ts
const options = {
allErrors: true,
verbose: true,
strict: "log"
};
const ajv$1 = {
draft04: (0, ajv_formats.default)(new ajv_draft_04.default(options)),
draft07: (0, ajv_formats.default)(new ajv.default(options)),
"draft2019-09": (0, ajv_formats.default)(new ajv_dist_2019.default(options)),
"draft2020-12": (0, ajv_formats.default)(new ajv_dist_2020.default(options))
};
function compileJsonSchema(schema) {
const schemaVersion = extractSchemaVersion(schema);
if (schemaVersion.includes("draft-04")) return ajv$1.draft04.compile(schema);
if (schemaVersion.includes("draft-07")) return ajv$1.draft07.compile(schema);
if (schemaVersion.includes("draft/2019-09")) return ajv$1["draft2019-09"].compile(schema);
if (schemaVersion.includes("draft/2020-12")) return ajv$1["draft2020-12"].compile(schema);
throw new Error(`Unsupported JSON Schema version "${schemaVersion}"`);
}
function formatJsonSchemaErrors(schema, errors) {
const schemaVersion = extractSchemaVersion(schema);
if (schemaVersion.includes("draft-04")) return ajv$1.draft04.errorsText(errors).replace(/^data\//gm, "");
if (schemaVersion.includes("draft-07")) return ajv$1.draft07.errorsText(errors).replace(/^data\//gm, "");
if (schemaVersion.includes("draft/2019-09")) return ajv$1["draft2019-09"].errorsText(errors).replace(/^data\//gm, "");
if (schemaVersion.includes("draft/2020-12")) return ajv$1["draft2020-12"].errorsText(errors).replace(/^data\//gm, "");
throw new Error(`Unsupported JSON Schema version "${schemaVersion}"`);
}
function getSchemaDefault(schema, labelForDebugging) {
try {
return (0, __nodecg_json_schema_defaults.default)(schema);
} catch (error) {
throw new Error(`Error generating default value(s) for schema "${labelForDebugging}":\n\t${stringifyError(error)}`);
}
}
const getSchemaDefaultFp = (schema, labelForDebugging) => fp_ts_Either.tryCatch(() => (0, __nodecg_json_schema_defaults.default)(schema), (error) => /* @__PURE__ */ new Error(`Error generating default value(s) for schema "${labelForDebugging}":\n\t${stringifyError(error)}`));
function extractSchemaVersion(schema) {
const defaultVersion = "https://json-schema.org/draft-04/schema";
const extractedVersion = schema.$schema;
return typeof extractedVersion === "string" ? extractedVersion : defaultVersion;
}
//#endregion
//#region src/shared/utils/isBrowser.ts
function isBrowser() {
return typeof globalThis.window !== "undefined";
}
function isWorker() {
return typeof globalThis.WorkerGlobalScope !== "undefined" && self instanceof globalThis.WorkerGlobalScope;
}
//#endregion
//#region src/shared/replicants.shared.ts
/**
* If you're wondering why some things are prefixed with "_",
* but not marked as protected or private, this is because our Proxy
* trap handlers need to access these parts of the Replicant internals,
* but don't have access to private or protected members.
*
* So, we code this like its 2010 and just use "_" on some public members.
*/
var AbstractReplicant = class extends TypedEmitter {
name;
namespace;
opts;
revision = 0;
log;
schema;
schemaSum;
status = "undeclared";
validationErrors = [];
_value;
_oldValue;
_operationQueue = [];
_pendingOperationFlush = false;
constructor(name, namespace, opts = {}) {
super();
if (!name || typeof name !== "string") throw new Error("Must supply a name when instantiating a Replicant");
if (!namespace || typeof namespace !== "string") throw new Error("Must supply a namespace when instantiating a Replicant");
if (typeof opts.persistent === "undefined") opts.persistent = true;
if (typeof opts.persistenceInterval === "undefined") opts.persistenceInterval = DEFAULT_PERSISTENCE_INTERVAL;
this.name = name;
this.namespace = namespace;
this.opts = opts;
const originalOnce = this.once.bind(this);
this.once = (event, listener) => {
if (event === "change" && this.status === "declared") {
listener(this.value);
return;
}
return originalOnce(event, listener);
};
/**
* When a new "change" listener is added, chances are that the developer wants it to be initialized ASAP.
* However, if this replicant has already been declared previously in this context, their "change"
* handler will *not* get run until another change comes in, which may never happen for Replicants
* that change very infrequently.
* To resolve this, we immediately invoke all new "change" handlers if appropriate.
*/
this.on("newListener", (event, listener) => {
if (event === "change" && this.status === "declared") listener(this.value);
});
}
/**
* If the operation is an array mutator method, call it on the target array with the operation arguments.
* Else, handle it with objectPath.
*/
_applyOperation(operation) {
ignoreProxy(this);
let result;
const path$13 = pathStrToPathArr(operation.path);
if (ARRAY_MUTATOR_METHODS.includes(operation.method)) {
if (typeof this.value !== "object" || this.value === null) throw new Error(`expected replicant "${this.namespace}:${this.name}" to have a value with type "object", got "${typeof this.value}" instead`);
const arr = object_path.default.get(this.value, path$13);
if (!Array.isArray(arr)) throw new Error(`expected to find an array in replicant "${this.namespace}:${this.name}" at path "${operation.path}"`);
result = arr[operation.method].apply(arr, "args" in operation && "mutatorArgs" in operation.args ? operation.args.mutatorArgs : []);
proxyRecursive(this, arr, operation.path);
} else switch (operation.method) {
case "overwrite": {
const { newValue } = operation.args;
this[isBrowser() || isWorker() ? "value" : "_value"] = proxyRecursive(this, newValue, operation.path);
result = true;
break;
}
case "add":
case "update": {
path$13.push(operation.args.prop);
let { newValue } = operation.args;
if (typeof newValue === "object") newValue = proxyRecursive(this, newValue, pathArrToPathStr(path$13));
result = object_path.default.set(this.value, path$13, newValue);
break;
}
case "delete":
if (path$13.length === 0 || object_path.default.has(this.value, path$13)) {
const target = object_path.default.get(this.value, path$13);
result = delete target[operation.args.prop];
}
break;
default:
/* istanbul ignore next */
throw new Error(`Unexpected operation method "${operation.method}"`);
}
resumeProxy(this);
return result;
}
/**
* Used to validate the new value of a replicant.
*
* This is a stub that will be replaced if a Schema is available.
*/
validate = () => true;
/**
* Generates a JSON Schema validator function from the `schema` property of the provided replicant.
* @param replicant {object} - The Replicant to perform the operation on.
* @returns {function} - The generated validator function.
*/
_generateValidator() {
const { schema } = this;
if (!schema) throw new Error("can't generate a validator for a replicant which lacks a schema");
let validate;
try {
validate = compileJsonSchema(schema);
} catch (error) {
throw new Error(`Error compiling JSON Schema for Replicant "${this.namespace}:${this.name}":\n\t${stringifyError(error)}`);
}
/**
* Validates a value against the current Replicant's schema.
* Throws when the value fails validation.
* @param [value=replicant.value] {*} - The value to validate. Defaults to the replicant's current value.
* @param [opts] {Object}
* @param [opts.throwOnInvalid = true] {Boolean} - Whether or not to immediately throw when the provided value fails validation against the schema.
*/
return function(value = this.value, { throwOnInvalid = true } = {}) {
const valid = validate(value);
if (!valid) {
this.validationErrors = validate.errors;
if (throwOnInvalid) throw new Error(`Invalid value rejected for replicant "${this.name}" in namespace "${this.namespace}":\n${formatJsonSchemaErrors(schema, validate.errors)}`);
}
return valid;
};
}
};
const proxyMetadataMap = /* @__PURE__ */ new WeakMap();
const metadataMap = /* @__PURE__ */ new WeakMap();
const proxySet = /* @__PURE__ */ new WeakSet();
const ignoringProxy = /* @__PURE__ */ new WeakSet();
const ARRAY_MUTATOR_METHODS = [
"copyWithin",
"fill",
"pop",
"push",
"reverse",
"shift",
"sort",
"splice",
"unshift"
];
/**
* The default persistence interval, in milliseconds.
*/
const DEFAULT_PERSISTENCE_INTERVAL = 100;
function ignoreProxy(replicant) {
ignoringProxy.add(replicant);
}
function resumeProxy(replicant) {
ignoringProxy.delete(replicant);
}
function isIgnoringProxy(replicant) {
return ignoringProxy.has(replicant);
}
const deleteTrap = function(target, prop) {
const metadata = metadataMap.get(target);
if (!metadata) throw new Error("arrived at delete trap without any metadata");
const { replicant } = metadata;
if (isIgnoringProxy(replicant)) return delete target[prop];
if (!{}.hasOwnProperty.call(target, prop)) return true;
if (replicant.schema) {
const valueClone = (0, klona_json.klona)(replicant.value);
const targetClone = object_path.default.get(valueClone, pathStrToPathArr(metadata.path));
delete targetClone[prop];
replicant.validate(valueClone);
}
replicant._addOperation({
path: metadata.path,
method: "delete",
args: { prop }
});
if (!isBrowser() && !isWorker()) return delete target[prop];
};
const CHILD_ARRAY_HANDLER = {
get(target, prop) {
const metadata = metadataMap.get(target);
if (!metadata) throw new Error("arrived at get trap without any metadata");
const { replicant } = metadata;
if (isIgnoringProxy(replicant)) return target[prop];
if ({}.hasOwnProperty.call(Array.prototype, prop) && typeof Array.prototype[prop] === "function" && target[prop] === Array.prototype[prop] && ARRAY_MUTATOR_METHODS.includes(prop)) return (...args) => {
if (replicant.schema) {
const valueClone = (0, klona_json.klona)(replicant.value);
const targetClone = object_path.default.get(valueClone, pathStrToPathArr(metadata.path));
targetClone[prop].apply(targetClone, args);
replicant.validate(valueClone);
}
if (isBrowser() || isWorker()) metada