Skip to content

Commit

Permalink
Update CLI Commands (#30)
Browse files Browse the repository at this point in the history
* add validate config commmand

* move files to server utils

* move utils package to server

* update error handler middleware

* add config path to start command

* remove process exit

* move comand

* move action functions to files

* update logger for start action

* update changeset
  • Loading branch information
gladwindos committed Mar 18, 2024
1 parent 84049b0 commit 4bee000
Show file tree
Hide file tree
Showing 37 changed files with 159 additions and 273 deletions.
7 changes: 7 additions & 0 deletions .changeset/serious-trees-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@gateweaver/policies": patch
"@gateweaver/server": patch
"@gateweaver/cli": patch
---

Update CLI
1 change: 1 addition & 0 deletions gateweaver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ endpoints:
test-header: "test"
policies:
- cors
- rateLimit
- path: "/todos"
method: "POST"
target:
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"license": "ISC",
"dependencies": {
"@gateweaver/server": "*",
"@gateweaver/utils": "*",
"@gateweaver/policies": "*",
"commander": "^12.0.0",
"dotenv": "^16.4.5"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crypto from "crypto";
import { hashApiKey } from "@gateweaver/utils";
import { hashApiKey } from "@gateweaver/policies";

const generateApiKey = (): string => {
return crypto.randomUUID().replace(/-/g, "");
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { startAction } from "./start";
export { validateAction } from "./validate";
export { generateApiKeyAction } from "./generate-api-key";
9 changes: 9 additions & 0 deletions packages/cli/src/actions/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { logger, startServer } from "@gateweaver/server/utils";

export const startAction = (configPath: string) => {
try {
startServer(configPath);
} catch (error) {
logger.error(error);
}
};
17 changes: 17 additions & 0 deletions packages/cli/src/actions/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { parseConfig, InvalidConfigError } from "@gateweaver/server/utils";

export const validateAction = (configPath: string) => {
try {
parseConfig(configPath);
console.log("✅ Config file is valid");
} catch (error) {
if (error instanceof InvalidConfigError) {
const validationErrors = error.message.split("\n");

console.error("❌ Config file validation errors:");
validationErrors.map((error) => console.error(`- ${error}`));
} else {
console.error((error as Error).message);
}
}
};
28 changes: 13 additions & 15 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#! /usr/bin/env node

import { config } from "dotenv";
config({ path: ".env.gateweaver" });
import { program } from "commander";
import { startServer } from "@gateweaver/server/start";
import { generateApiKeyAction } from "./generate-api-key";
import { startAction, validateAction, generateApiKeyAction } from "./actions";

const setupCLI = async () => {
const packageJson = await import("../package.json", {
Expand All @@ -16,23 +14,23 @@ const setupCLI = async () => {
.description("A CLI tool for managing gateweaver gateways")
.version(packageJson.default.version);

program
.command("start")
.description("Start the gateweaver server")
.argument("[configPath]", "Path to the config file", "gateweaver")
.action(startAction);

program
.command("validate")
.description("Validate a gateweaver config file")
.argument("[configPath]", "Path to the config file", "gateweaver")
.action(validateAction);

program
.command("generate-api-key")
.description("Generate a new API key and hash")
.action(generateApiKeyAction);

program
.command("start")
.description("Start the gateweaver server")
.action(() => {
try {
startServer();
} catch (error) {
console.error(error);
process.exit(1);
}
});

program.parse();
};

Expand Down
1 change: 0 additions & 1 deletion packages/policies/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"author": "",
"license": "ISC",
"dependencies": {
"@gateweaver/utils": "workspace:*",
"ajv": "^8.12.0",
"cors": "^2.8.5",
"express-jwt": "^8.4.1",
Expand Down
7 changes: 5 additions & 2 deletions packages/policies/src/api-key/api-key.middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Request, Response } from "express";
import { hashApiKey } from "@gateweaver/utils";
import { apiKeyMiddleware, isValidApiKey } from "./api-key.middleware";
import {
apiKeyMiddleware,
hashApiKey,
isValidApiKey,
} from "./api-key.middleware";

describe("isValidApiKey", () => {
it("returns true for a valid API key", () => {
Expand Down
17 changes: 11 additions & 6 deletions packages/policies/src/api-key/api-key.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import crypto from "crypto";
import { Request, Response, NextFunction, RequestHandler } from "express";
import { hashApiKey } from "@gateweaver/utils";
import { ApiKeyPolicy } from "./api-key.schema";

export const isValidApiKey = (apiKey: string, apiKeyHashes: string[]) => {
export const hashApiKey = (apiKey: string): string => {
return crypto.createHash("sha256").update(apiKey).digest("hex");
};

export const isValidApiKey = (
apiKey: string | string[],
apiKeyHashes: string[],
) => {
if (typeof apiKey !== "string") return false;

const apiKeyHash = hashApiKey(apiKey);
return apiKeyHashes.includes(apiKeyHash);
};
Expand All @@ -18,10 +27,6 @@ export const apiKeyMiddleware = (policy: ApiKeyPolicy): RequestHandler => {
return;
}

if (typeof apiKey !== "string") {
throw new Error("API key is not a string");
}

if (!isValidApiKey(apiKey, policy.apiKeyHashes)) {
res.status(401).send({
error: "Invalid API Key",
Expand Down
1 change: 1 addition & 0 deletions packages/policies/src/rate-limit/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { RateLimitUnauthorizedError } from "./key-generator";
export * from "./rate-limit.schema";
export * from "./rate-limit.middleware";
7 changes: 3 additions & 4 deletions packages/policies/src/rate-limit/key-generator.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Request } from "express";
import { UnauthorizedAccessError } from "@gateweaver/utils";
import { keyGenerator } from "./key-generator";
import { RateLimitUnauthorizedError, keyGenerator } from "./key-generator";
import { RateLimitBy } from "./rate-limit.schema";

describe("keyGenerator", () => {
Expand All @@ -25,7 +24,7 @@ describe("keyGenerator", () => {

it("should throw an error if API key is missing and rate limiting by API key", () => {
expect(() => keyGenerator(RateLimitBy.API_KEY)(req)).toThrow(
new UnauthorizedAccessError("API Key Required"),
new RateLimitUnauthorizedError("API Key Required"),
);
});

Expand All @@ -42,7 +41,7 @@ describe("keyGenerator", () => {

it("should throw an error if JWT is missing and rate limiting by JWT", () => {
expect(() => keyGenerator(RateLimitBy.JWT)(req)).toThrow(
new UnauthorizedAccessError("No authorization token was found"),
new RateLimitUnauthorizedError("No authorization token was found"),
);
});

Expand Down
16 changes: 12 additions & 4 deletions packages/policies/src/rate-limit/key-generator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Request } from "express";
import { UnauthorizedAccessError } from "@gateweaver/utils";
import { RateLimitBy } from "./rate-limit.schema";

export class RateLimitUnauthorizedError extends Error {
constructor(message: string) {
super(message);
this.name = "RateLimitUnauthorizedError";
}
}

const decodeJwt = (token: string) => {
try {
return JSON.parse(atob(token.split(".")[1]));
} catch (e) {
throw new UnauthorizedAccessError("Invalid Token");
throw new RateLimitUnauthorizedError("Invalid Token");
}
};

Expand All @@ -16,7 +22,7 @@ export const keyGenerator = (option: RateLimitBy) => {
case RateLimitBy.API_KEY: {
const apiKey = req.headers["x-api-key"];
if (!apiKey) {
throw new UnauthorizedAccessError("API Key Required");
throw new RateLimitUnauthorizedError("API Key Required");
}

return apiKey;
Expand All @@ -25,7 +31,9 @@ export const keyGenerator = (option: RateLimitBy) => {
const authHeader = req.headers.authorization;

if (!authHeader) {
throw new UnauthorizedAccessError("No authorization token was found");
throw new RateLimitUnauthorizedError(
"No authorization token was found",
);
}

const token = authHeader.split(" ")[1];
Expand Down
14 changes: 8 additions & 6 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./start": {
"import": "./dist/start-server.mjs",
"require": "./dist/start-server.js",
"types": "./dist/start-server.d.ts"
"./utils": {
"import": "./dist/utils/index.mjs",
"require": "./dist/utils/index.js",
"types": "./dist/utils/index.d.ts"
}
},
"scripts": {
Expand All @@ -28,16 +28,18 @@
"license": "ISC",
"dependencies": {
"@gateweaver/policies": "workspace:*",
"@gateweaver/utils": "workspace:*",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"helmet": "^7.1.0",
"http-proxy-middleware": "^2.0.6",
"pino": "^8.19.0",
"pino-pretty": "^10.3.1",
"yaml": "^2.3.4"
},
"devDependencies": {
"@types/express": "^4.17.21"
"@types/express": "^4.17.21",
"express-jwt": "^8.4.1"
}
}
4 changes: 2 additions & 2 deletions packages/server/src/config/validate-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe("validateConfig", () => {
};

expect(() => validateConfig(invalidConfig)).toThrow(
'Duplicate endpoint path/method combination: "GET /duplicate"',
"Duplicate endpoint path/method combination: GET /duplicate",
);
});

Expand All @@ -65,7 +65,7 @@ describe("validateConfig", () => {
};

expect(() => validateConfig(invalidConfig)).toThrow(
'Invalid path "invalid path". Must start with / and only contain alphanumeric characters, hyphens, and underscores',
"Invalid path: 'invalid path'. Must start with / and only contain alphanumeric characters, hyphens, and underscores",
);
});

Expand Down
12 changes: 5 additions & 7 deletions packages/server/src/config/validate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PolicyDefinitions, PolicyOption } from "@gateweaver/policies";
import { configSchema } from "./config.schema";
import { Endpoint, Config } from "./config.types";

class InvalidConfigError extends Error {
export class InvalidConfigError extends Error {
constructor(message: string) {
super(message);
this.name = "InvalidConfigError";
Expand All @@ -18,7 +18,7 @@ const checkEndpointUniqueness = (
const pathKey = `${endpoint.method} ${endpoint.path}`;

if (endpointPaths.has(pathKey)) {
return `Duplicate endpoint path/method combination: "${pathKey}"`;
return `Duplicate endpoint path/method combination: ${pathKey}`;
} else {
endpointPaths.add(pathKey);
return null;
Expand All @@ -28,7 +28,7 @@ const checkEndpointUniqueness = (
const validateEndpointPath = (endpoint: Endpoint): string | null => {
const pathPattern = /^\/[a-zA-Z0-9\-_/]*\/?$/;
if (!pathPattern.test(endpoint.path)) {
return `Invalid path "${endpoint.path}". Must start with / and only contain alphanumeric characters, hyphens, and underscores`;
return `Invalid path: '${endpoint.path}'. Must start with / and only contain alphanumeric characters, hyphens, and underscores`;
}
return null;
};
Expand Down Expand Up @@ -104,16 +104,14 @@ export const validateConfig = (config: Config): Config => {
const validationErrors: string[] = [];

if (!validate(config)) {
console.log(validate.errors);

const errorMessages = validate.errors?.map((error) => {
const instancePath = error.instancePath
.replace(/^\//, "")
.replace(/\//g, ".");

const allowedValues = error.params?.allowedValues?.join(", ");

return `${instancePath} ${error.message} ${allowedValues ? `(${allowedValues})` : ""}`;
return `${instancePath} ${error.message}${allowedValues ? ` (${allowedValues})` : ""}`;
});

if (errorMessages) validationErrors.push(...errorMessages);
Expand Down Expand Up @@ -146,7 +144,7 @@ export const validateConfig = (config: Config): Config => {
}

if (validationErrors.length > 0) {
throw new InvalidConfigError(`${validationErrors.join("; ")}`);
throw new InvalidConfigError(validationErrors.join("\n"));
}

return config;
Expand Down
13 changes: 9 additions & 4 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { config } from "dotenv";
config({ path: ".env.gateweaver" });
import { logger } from "@gateweaver/utils";
import { startServer } from "./start-server";
import { startServer, logger, InvalidConfigError } from "./utils";

try {
startServer();
} catch (error) {
logger.error(error);
process.exit(1);
if (error instanceof InvalidConfigError) {
const validationErrors = error.message.split("\n");

logger.error("Invalid config file");
logger.error(validationErrors);
} else {
logger.error(error);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { NextFunction, Request, Response } from "express";
import { logger } from "../logger";

export class UnauthorizedAccessError extends Error {
constructor(message: string) {
super(message);
this.name = "UnauthorizedAccessError";
}
}
import { UnauthorizedError } from "express-jwt";
import { RateLimitUnauthorizedError } from "@gateweaver/policies";
import { logger } from "../utils/logger";

export const errorHandler = (
err: Error,
Expand All @@ -15,16 +10,14 @@ export const errorHandler = (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
next: NextFunction,
) => {
// express-jwt middleware error
if (err.name === "UnauthorizedError") {
if (err instanceof UnauthorizedError) {
res.status(401).send({
error: "Invalid Token",
});
return;
}

// custom unauthorized access error
if (err.name === "UnauthorizedAccessError") {
if (err instanceof RateLimitUnauthorizedError) {
res.status(401).send({
error: err.message,
});
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { errorHandler } from "./error-handler.middleware";
Loading

0 comments on commit 4bee000

Please sign in to comment.