@godspeedsystems/plugins-express-as-http
Version:
Godspeed event source plugin for express as http server
382 lines • 19 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_CONFIG = exports.CONFIG_FILE_NAME = exports.Type = exports.SourceType = exports.EventSource = void 0;
const core_1 = require("@godspeedsystems/core");
const express_1 = __importDefault(require("express"));
const body_parser_1 = __importDefault(require("body-parser"));
const metrics_1 = __importDefault(require("@godspeedsystems/metrics"));
const cors_1 = __importDefault(require("cors"));
//@ts-ignore
const express_prometheus_middleware_1 = __importDefault(require("@godspeedsystems/express-prometheus-middleware"));
const passport_1 = __importDefault(require("passport"));
const session = require("express-session");
const express_fileupload_1 = __importDefault(require("express-fileupload"));
const passport_jwt_1 = require("passport-jwt");
const passport_github2_1 = require("passport-github2");
const passport_linkedin_oauth2_1 = require("passport-linkedin-oauth2");
const passport_google_oauth2_1 = require("passport-google-oauth2");
class EventSource extends core_1.GSEventSource {
initClient() {
return __awaiter(this, void 0, void 0, function* () {
const app = (0, express_1.default)();
const { port = 3000, docs = { endpoint: "/api-docs" } } = this.config;
this.setupMiddleware(app);
this.setupAuthentication(app);
// Start the Express server
app.listen(port, () => {
core_1.logger.info(`Server running on port ${port}`);
core_1.logger.info(`Try it out at: http://localhost:${port}${docs.endpoint}`);
});
if (process.env.OTEL_ENABLED === "true") {
this.setupMetrics(app);
}
return app;
});
}
setupMiddleware(app) {
var _a;
app.use((0, cors_1.default)());
app.use(body_parser_1.default.urlencoded({
extended: true,
limit: this.config.request_body_limit || "50mb",
}));
app.use(body_parser_1.default.json({ limit: this.config.file_size_limit || "50mb" }));
app.use((0, express_fileupload_1.default)({
useTempFiles: true,
limits: { fileSize: this.config.file_size_limit || "50mb" },
abortOnLimit: true,
}));
app.use(session({
secret: ((_a = this.config.session) === null || _a === void 0 ? void 0 : _a.secret) || "mysecret",
resave: false,
saveUninitialized: false,
}));
}
setupAuthentication(app) {
var _a, _b, _c, _d, _e, _f, _g;
const jwtConfig = ((_a = this.config.authn) === null || _a === void 0 ? void 0 : _a.jwt) || this.config.jwt;
const githubConfig = (_c = (_b = this.config.authn) === null || _b === void 0 ? void 0 : _b.oauth2) === null || _c === void 0 ? void 0 : _c.github;
const googleConfig = (_e = (_d = this.config.authn) === null || _d === void 0 ? void 0 : _d.oauth2) === null || _e === void 0 ? void 0 : _e.google;
const linkedinConfig = (_g = (_f = this.config.authn) === null || _f === void 0 ? void 0 : _f.oauth2) === null || _g === void 0 ? void 0 : _g.linkedin;
if (jwtConfig) {
this.setupJwtAuthentication(app, jwtConfig);
}
if (googleConfig) {
this.setupGoogleAuthentication(app, googleConfig);
}
if (githubConfig) {
this.setupGithubAuthentication(app, githubConfig);
}
if (linkedinConfig) {
this.setupLinkedInAuthentication(app, linkedinConfig);
}
}
setupJwtAuthentication(app, jwtConfig) {
if (!jwtConfig.secretOrKey || !jwtConfig.audience || !jwtConfig.issuer) {
core_1.logger.fatal("JWT configuration error. Exiting");
process.exit(1);
}
app.use(passport_1.default.initialize());
app.use(passport_1.default.session());
passport_1.default.use(new passport_jwt_1.Strategy({
jwtFromRequest: passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtConfig.secretOrKey,
ignoreExpiration: (jwtConfig === null || jwtConfig === void 0 ? void 0 : jwtConfig.ignoreExpiration) || false,
jsonWebTokenOptions: {
audience: jwtConfig.audience,
issuer: jwtConfig.issuer,
},
}, (jwtPayload, done) => done(null, jwtPayload)));
}
setupGoogleAuthentication(app, googleConfig) {
if (!googleConfig.client_id ||
!googleConfig.client_secret ||
!googleConfig.callback_url) {
core_1.logger.fatal("Google configuration error. Exiting");
process.exit(1);
}
app.use(passport_1.default.initialize());
app.use(passport_1.default.session());
passport_1.default.use(new passport_google_oauth2_1.Strategy({
clientID: googleConfig.client_id,
clientSecret: googleConfig.client_secret,
callbackURL: googleConfig.callback_url,
scope: googleConfig.scope || ["email", "profile"],
passReqToCallback: true, // Enable this option
}, (req, accessToken, refreshToken, params, profile, done) => {
var _a;
const stateParam = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state;
const state = this.parseJsonIfPossible(stateParam ? decodeURIComponent(stateParam.toString()) : null);
return done(null, Object.assign(Object.assign({}, profile), { $state: state }));
}));
const authRoute = googleConfig.auth_route || "/auth/google";
const callbackRoute = googleConfig.callback_route || "/auth/google/callback";
const failureRedirectURL = googleConfig.failure_redirect || "/error";
const successRedirectURL = googleConfig.success_redirect || "/verify/user";
// ***************Authentication routes *********************
app.get(authRoute, (req, ...args) => {
var _a;
const queryState = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state;
const state = queryState ? encodeURIComponent(queryState.toString()) : "";
passport_1.default.authenticate("google", { state })(req, ...args);
});
app.get(callbackRoute, passport_1.default.authenticate("google", {
failureRedirect: failureRedirectURL,
}), (req, res) => __awaiter(this, void 0, void 0, function* () {
res.redirect(successRedirectURL);
}));
passport_1.default.serializeUser(function (user, done) {
done(null, user);
});
passport_1.default.deserializeUser(function (obj, done) {
done(null, obj);
});
}
setupLinkedInAuthentication(app, linkedinConfig) {
if (!linkedinConfig.client_id ||
!linkedinConfig.client_secret ||
!linkedinConfig.callback_url) {
core_1.logger.fatal("LinkedIn configuration error. Exiting");
process.exit(1);
}
app.use(passport_1.default.initialize());
app.use(passport_1.default.session());
passport_1.default.use(new passport_linkedin_oauth2_1.Strategy({
clientID: linkedinConfig.client_id,
clientSecret: linkedinConfig.client_secret,
callbackURL: linkedinConfig.callback_url,
scope: linkedinConfig.scope || ["openid", "email", "profile"],
passReqToCallback: true,
}, (req, accessToken, refreshToken, profile, done) => {
var _a;
const stateParam = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state;
const state = this.parseJsonIfPossible(stateParam ? decodeURIComponent(stateParam.toString()) : null);
return done(null, Object.assign(Object.assign({}, profile), { $state: state }));
}));
const authRoute = linkedinConfig.auth_route || "/auth/linkedin";
const callbackRoute = linkedinConfig.callback_route || "/auth/linkedin/callback";
const failureRedirectURL = linkedinConfig.failure_redirect || "/error";
const successRedirectURL = linkedinConfig.success_redirect || "/verify/user";
// LinkedIn Authentication Routes
app.get(authRoute, (req, ...args) => {
var _a;
const queryState = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state;
const state = queryState ? encodeURIComponent(queryState.toString()) : "";
passport_1.default.authenticate("linkedin", { state })(req, ...args);
});
app.get(callbackRoute, passport_1.default.authenticate("linkedin", {
failureRedirect: failureRedirectURL,
}), (req, res) => {
res.redirect(successRedirectURL);
});
passport_1.default.serializeUser((user, done) => done(null, user));
passport_1.default.deserializeUser((obj, done) => done(null, obj));
}
setupGithubAuthentication(app, githubConfig) {
if (!githubConfig.client_id ||
!githubConfig.client_secret ||
!githubConfig.callback_url) {
core_1.logger.fatal("Github configuration error. Exiting");
process.exit(1);
}
app.use(passport_1.default.initialize());
app.use(passport_1.default.session());
passport_1.default.use(new passport_github2_1.Strategy({
clientID: githubConfig.client_id,
clientSecret: githubConfig.client_secret,
callbackURL: githubConfig.callback_url,
scope: githubConfig.scope || ["user:email"],
passReqToCallback: true,
}, (req, accessToken, refreshToken, result, profile, done) => {
var _a;
const stateParam = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state;
const state = this.parseJsonIfPossible(stateParam ? decodeURIComponent(stateParam.toString()) : null);
return done(null, Object.assign(Object.assign({}, profile), { $state: state }));
}));
const authRoute = githubConfig.auth_route || "/auth/github";
const callbackRoute = githubConfig.callback_route || "/auth/github/callback";
const failureRedirectURL = githubConfig.failure_redirect || "/error";
const successRedirectURL = githubConfig.success_redirect || "/verify/user";
// ************* Authentication routes GITHUB ******************
app.get(authRoute, passport_1.default.authenticate("github", { session: true, scope: ["user:email"] }));
app.get(callbackRoute, passport_1.default.authenticate("github", { failureRedirect: failureRedirectURL }), (req, res) => __awaiter(this, void 0, void 0, function* () {
res.redirect(successRedirectURL);
}));
passport_1.default.serializeUser((user, done) => done(null, user));
passport_1.default.deserializeUser((obj, done) => done(null, obj));
}
// Setup OpenTelemetry metrics
setupMetrics(app) {
const { metrics } = this.config;
// Validation helper
const validateBuckets = (buckets, name) => {
if (!buckets)
return null;
if (!Array.isArray(buckets) ||
!buckets.every((b) => typeof b === "number" && !isNaN(b))) {
throw new Error(`${name} must be an array of numbers`);
}
return buckets;
};
app.use((0, express_prometheus_middleware_1.default)({
metricsPath: (metrics === null || metrics === void 0 ? void 0 : metrics.metricsPath) || "/metrics",
collectDefaultMetrics: true,
requestDurationBuckets: validateBuckets(metrics === null || metrics === void 0 ? void 0 : metrics.requestDurationBuckets, "requestDurationBuckets") || metrics_1.default.exponentialBuckets(0.2, 3, 6),
requestLengthBuckets: validateBuckets(metrics === null || metrics === void 0 ? void 0 : metrics.requestLengthBuckets, "requestLengthBuckets") || metrics_1.default.exponentialBuckets(512, 2, 10),
responseLengthBuckets: validateBuckets(metrics === null || metrics === void 0 ? void 0 : metrics.responseLengthBuckets, "responseLengthBuckets") || metrics_1.default.exponentialBuckets(512, 2, 10),
// customLabels: ['traceId', 'spanId', 'traceFlags'], // Add custom label
// transformLabels: (labels: any, req: any, res: any) => {
// const span = trace.getSpan(context.active());
// if (span) {
// const ctx = span.spanContext();
// labels.traceId = ctx.traceId;
// labels.spanId = ctx.spanId;
// labels.traceFlags = ctx.traceFlags.toString(); // usually 1 or 0
// } else {
// labels.traceId = 'unknown';
// labels.spanId = 'unknown';
// labels.traceFlags = '0';
// }
// }
}));
}
authnHOF(authn) {
return (req, res, next) => {
var _a, _b, _c, _d;
if ((authn !== false || (authn === false && req.headers.authorization)) && (((_a = this.config.authn) === null || _a === void 0 ? void 0 : _a.jwt) || this.config.authn)) {
return passport_1.default.authenticate("jwt", { session: false })(req, res, next);
}
if (authn !== false &&
(((_b = this.config.authn) === null || _b === void 0 ? void 0 : _b.oauth2) || ((_d = (_c = this.config.authn) === null || _c === void 0 ? void 0 : _c.oauth2) === null || _d === void 0 ? void 0 : _d.google))) {
req.user ? next() : res.sendStatus(401);
return passport_1.default.authenticate("google", { scope: ["email", "profile"] });
}
else {
next();
}
};
}
subscribeToEvent(eventRoute, eventConfig, processEvent, event) {
const routeSplit = eventRoute.split(".");
const httpMethod = routeSplit[1];
let endpoint = routeSplit[2].replace(/{(.*?)}/g, ":$1");
let baseUrl = this.config.base_url;
let fullUrl;
if (baseUrl) {
fullUrl = "/" + baseUrl + "/" + endpoint;
fullUrl = fullUrl.replace(/\/\//g, "/");
}
else {
fullUrl = endpoint;
}
const app = this.client;
//@ts-ignore
app[httpMethod](fullUrl, this.authnHOF(event === null || event === void 0 ? void 0 : event.authn), (req, res) => __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const gsEvent = createGSEvent(req, endpoint);
const status = yield processEvent(gsEvent, Object.assign({ key: eventRoute }, eventConfig));
if (status.code && ((_a = status.data) === null || _a === void 0 ? void 0 : _a.redirectUrl) && status.code === 302) {
res.redirect(302, status.data.redirectUrl);
}
else {
res
.status(status.code || 200)
// if data is a integer, it takes it as statusCode, so explicitly converting it to string
.send(Number.isInteger(status.data)
? String(status.data)
: status.data);
}
}
catch (err) {
console.error("Unhandled error in API handler:", err);
res.status(500).json({
error: "Internal server error during request handling.",
details: err instanceof Error ? err.message : err,
});
}
}));
return Promise.resolve();
}
parseJsonIfPossible(value) {
if (!value)
return null;
const decoded = value;
try {
const parsed = JSON.parse(decoded);
// Only return parsed if it's an object, array, number, boolean, or null
if (typeof parsed === "object" ||
typeof parsed === "number" ||
typeof parsed === "boolean") {
return parsed;
}
// Otherwise, fallback to string
return decoded;
}
catch (_a) {
// Not valid JSON, return as plain string
return decoded;
}
}
}
exports.default = EventSource;
exports.EventSource = EventSource;
// Remove leading and trailing / (slash) if present
function trimSlashes(endpoint) {
if (endpoint[0] === "/") {
endpoint = endpoint.substring(1);
}
if (endpoint[endpoint.length - 1] === "/") {
endpoint = endpoint.substring(0, endpoint.length - 1);
}
return endpoint;
}
function createGSEvent(req, endpoint) {
const reqProp = omit(req, [
"_readableState",
"socket",
"client",
"_parsedUrl",
"res",
"app",
]);
const reqHeaders = pick(req, ["headers"]);
let data = Object.assign(Object.assign({}, reqProp), reqHeaders);
const event = new core_1.GSCloudEvent("id", endpoint, new Date(), "http", "1.0", data, "REST", new core_1.GSActor("user"), {});
return event;
}
function pick(o, keys) {
let newObj = {};
for (let key of keys) {
newObj[key] = o[key];
}
return newObj; //return new copy
}
function omit(o, keys) {
o = Object.assign({}, o); //shallow clone
for (let key of keys) {
delete o[key];
}
return o; //return new copy
}
const SourceType = "ES";
exports.SourceType = SourceType;
const Type = "express"; // this is the loader file of the plugin, So the final loader file will be `types/${Type.js}`
exports.Type = Type;
const CONFIG_FILE_NAME = "http"; // in case of event source, this also works as event identifier, and in case of datasource works as datasource name
exports.CONFIG_FILE_NAME = CONFIG_FILE_NAME;
const DEFAULT_CONFIG = { port: 3000, docs: { endpoint: "/api-docs" } };
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
//# sourceMappingURL=index.js.map