mirror of
https://github.com/LukeHagar/arbiter.git
synced 2025-12-06 04:19:14 +00:00
updating lint, format, and CI
This commit is contained in:
64
.eslintrc.json
Normal file
64
.eslintrc.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es2022": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}],
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "warn",
|
||||||
|
"@typescript-eslint/require-await": "warn",
|
||||||
|
"no-console": ["warn", {
|
||||||
|
"allow": ["warn", "error", "info", "log"]
|
||||||
|
}],
|
||||||
|
"eqeqeq": ["error", "always"],
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"curly": ["error", "all"]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/unbound-method": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignorePatterns": ["dist", "node_modules", "coverage", "*.config.mjs"],
|
||||||
|
"root": true
|
||||||
|
}
|
||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -26,7 +26,8 @@ jobs:
|
|||||||
run: npm run format:check
|
run: npm run format:check
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint
|
run: npm run lint -- --max-warnings 0
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Tests
|
name: Tests
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
"test:ci": "vitest run",
|
"test:ci": "vitest run",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint:fix": "eslint . --ext .ts --fix",
|
"lint:fix": "eslint . --ext .ts --fix",
|
||||||
"format": "prettier --write \"src/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"integration/**/*.ts\"",
|
||||||
"format:check": "prettier --check \"src/**/*.ts\""
|
"format:check": "prettier --check \"src/**/*.ts\" \"integration/**/*.ts\""
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"api",
|
"api",
|
||||||
|
|||||||
@@ -1,25 +1,53 @@
|
|||||||
import { Context } from 'hono';
|
import type { Context, Next } from 'hono';
|
||||||
import { openApiStore } from '../store/openApiStore.js';
|
import type { OpenAPIStore } from '../store/openApiStore.js';
|
||||||
|
|
||||||
export const apiDocGenerator = async (c: Context, next: () => Promise<void>) => {
|
export function apiDocGenerator(store: OpenAPIStore): (c: Context, next: Next) => Promise<void> {
|
||||||
|
return async (c: Context, next: Next): Promise<void> => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
await next();
|
await next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in apiDocGenerator middleware:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// Record the API call in OpenAPI format
|
const endTime = Date.now();
|
||||||
openApiStore.recordEndpoint(
|
const responseTime = endTime - startTime;
|
||||||
|
|
||||||
|
// Get request details
|
||||||
|
const url = new URL(c.req.url);
|
||||||
|
const queryParams: Record<string, string> = {};
|
||||||
|
for (const [key, value] of url.searchParams.entries()) {
|
||||||
|
queryParams[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get request headers
|
||||||
|
const requestHeaders: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(c.req.header())) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
requestHeaders[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the endpoint in OpenAPI format
|
||||||
|
try {
|
||||||
|
store.recordEndpoint(
|
||||||
c.req.path,
|
c.req.path,
|
||||||
c.req.method.toLowerCase(),
|
c.req.method.toLowerCase(),
|
||||||
{
|
{
|
||||||
query: Object.fromEntries(new URL(c.req.url).searchParams),
|
query: queryParams,
|
||||||
body: await c.req.json().catch(() => null),
|
headers: requestHeaders,
|
||||||
contentType: c.req.header('content-type') || 'application/json',
|
contentType: c.req.header('content-type') || 'application/json',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: c.res.status,
|
status: c.res.status,
|
||||||
body: await c.res
|
|
||||||
.clone()
|
|
||||||
.json()
|
|
||||||
.catch(() => null),
|
|
||||||
contentType: c.res.headers.get('content-type') || 'application/json',
|
contentType: c.res.headers.get('content-type') || 'application/json',
|
||||||
|
headers: Object.fromEntries(c.res.headers.entries()),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error recording endpoint:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Context, Next } from 'hono';
|
import type { Context, Next } from 'hono';
|
||||||
import { openApiStore } from '../store/openApiStore.js';
|
import type { OpenAPIStore } from '../store/openApiStore.js';
|
||||||
import { SecurityInfo } from '../store/openApiStore.js';
|
import { SecurityInfo } from '../store/openApiStore.js';
|
||||||
|
|
||||||
interface HAREntry {
|
interface HAREntry {
|
||||||
@@ -29,92 +29,33 @@ interface HAREntry {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function harRecorder(c: Context, next: Next) {
|
export function harRecorder(store: OpenAPIStore): (c: Context, next: Next) => Promise<void> {
|
||||||
|
return async (c: Context, next: Next): Promise<void> => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Get request body if present
|
|
||||||
let requestBody: any;
|
|
||||||
const contentType = c.req.header('content-type') || 'application/json';
|
|
||||||
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
|
|
||||||
try {
|
try {
|
||||||
const body = await c.req.text();
|
|
||||||
if (contentType.includes('application/json')) {
|
|
||||||
requestBody = JSON.parse(body);
|
|
||||||
} else {
|
|
||||||
requestBody = body;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Body might not be valid JSON or might be empty
|
|
||||||
requestBody = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get query parameters from URL
|
|
||||||
const url = new URL(c.req.url);
|
|
||||||
const queryParams: Record<string, string> = {};
|
|
||||||
for (const [key, value] of url.searchParams.entries()) {
|
|
||||||
queryParams[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all request headers
|
|
||||||
const requestHeaders: Record<string, string> = {};
|
|
||||||
Object.entries(c.req.header()).forEach(([key, value]) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
requestHeaders[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for security schemes
|
|
||||||
const security: SecurityInfo[] = [];
|
|
||||||
const apiKey = c.req.header('x-api-key');
|
|
||||||
if (apiKey) {
|
|
||||||
security.push({
|
|
||||||
type: 'apiKey',
|
|
||||||
name: 'x-api-key',
|
|
||||||
in: 'header',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call next middleware
|
|
||||||
await next();
|
await next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in harRecorder middleware:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate response time
|
const endTime = Date.now();
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = endTime - startTime;
|
||||||
|
|
||||||
// Get response body
|
// Record the request/response in HAR format
|
||||||
let responseBody: any;
|
|
||||||
const responseContentType = c.res.headers.get('content-type') || 'application/json';
|
|
||||||
try {
|
try {
|
||||||
const body = await c.res.clone().text();
|
store.recordHAREntry({
|
||||||
if (responseContentType.includes('application/json')) {
|
request: c.req.raw,
|
||||||
responseBody = JSON.parse(body);
|
response: c.res,
|
||||||
} else {
|
timing: {
|
||||||
responseBody = body;
|
wait: responseTime,
|
||||||
}
|
receive: 0,
|
||||||
} catch (e) {
|
send: 0,
|
||||||
// Response body might not be valid JSON or might be empty
|
|
||||||
responseBody = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record the request/response in OpenAPI format
|
|
||||||
openApiStore.recordEndpoint(
|
|
||||||
c.req.path,
|
|
||||||
c.req.method.toLowerCase(),
|
|
||||||
{
|
|
||||||
query: queryParams,
|
|
||||||
body: requestBody,
|
|
||||||
contentType,
|
|
||||||
headers: requestHeaders,
|
|
||||||
security,
|
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
status: c.res.status,
|
} catch (error) {
|
||||||
body: responseBody,
|
console.error('Error recording HAR entry:', error);
|
||||||
contentType: responseContentType,
|
|
||||||
headers: Object.fromEntries(c.res.headers.entries()),
|
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
// Set HAR data in context
|
|
||||||
c.set('har', openApiStore.generateHAR());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { openApiStore } from './store/openApiStore.js';
|
|||||||
import { IncomingMessage, ServerResponse, createServer, Server } from 'node:http';
|
import { IncomingMessage, ServerResponse, createServer, Server } from 'node:http';
|
||||||
import { Agent } from 'node:https';
|
import { Agent } from 'node:https';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { harRecorder } from './middleware/harRecorder.js';
|
||||||
|
import { apiDocGenerator } from './middleware/apiDocGenerator.js';
|
||||||
|
import type { ServerConfig } from './types.js';
|
||||||
|
|
||||||
export interface ServerOptions {
|
export interface ServerOptions {
|
||||||
target: string;
|
target: string;
|
||||||
@@ -61,6 +64,16 @@ export async function startServers(
|
|||||||
docsApp.use('*', cors());
|
docsApp.use('*', cors());
|
||||||
docsApp.use('*', prettyJSON());
|
docsApp.use('*', prettyJSON());
|
||||||
|
|
||||||
|
// Configure proxy server middleware
|
||||||
|
proxyApp.use('*', async (c, next) => {
|
||||||
|
await harRecorder(openApiStore)(c);
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
proxyApp.use('*', async (c, next) => {
|
||||||
|
await apiDocGenerator(openApiStore)(c);
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
// Documentation endpoints
|
// Documentation endpoints
|
||||||
docsApp.get('/docs', async (c: Context) => {
|
docsApp.get('/docs', async (c: Context) => {
|
||||||
const spec = openApiStore.getOpenAPISpec();
|
const spec = openApiStore.getOpenAPISpec();
|
||||||
@@ -381,3 +394,10 @@ export async function startServers(
|
|||||||
|
|
||||||
return { proxyServer, docsServer };
|
return { proxyServer, docsServer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createServerConfig(app: Hono, port: number): ServerConfig {
|
||||||
|
return {
|
||||||
|
fetch: app.fetch,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type { OpenAPI, OpenAPIV3_1 } from 'openapi-types';
|
|||||||
|
|
||||||
export interface SecurityInfo {
|
export interface SecurityInfo {
|
||||||
type: 'apiKey' | 'oauth2' | 'http' | 'openIdConnect';
|
type: 'apiKey' | 'oauth2' | 'http' | 'openIdConnect';
|
||||||
scheme?: 'bearer' | 'basic' | 'digest';
|
|
||||||
name?: string;
|
name?: string;
|
||||||
in?: 'header' | 'query' | 'cookie';
|
in?: 'header' | 'query' | 'cookie';
|
||||||
|
scheme?: string;
|
||||||
flows?: {
|
flows?: {
|
||||||
implicit?: {
|
implicit?: {
|
||||||
authorizationUrl: string;
|
authorizationUrl: string;
|
||||||
@@ -91,7 +91,7 @@ type PathsObject = {
|
|||||||
[path: string]: PathItemObject;
|
[path: string]: PathItemObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
class OpenAPIStore {
|
export class OpenAPIStore {
|
||||||
private endpoints: Map<string, EndpointInfo>;
|
private endpoints: Map<string, EndpointInfo>;
|
||||||
private harEntries: HAREntry[];
|
private harEntries: HAREntry[];
|
||||||
private targetUrl: string;
|
private targetUrl: string;
|
||||||
|
|||||||
6
src/types.ts
Normal file
6
src/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Hono } from 'hono';
|
||||||
|
|
||||||
|
export interface ServerConfig {
|
||||||
|
fetch: Hono['fetch'];
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
/* Modules */
|
/* Modules */
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"rootDir": "./src",
|
"rootDir": ".",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
"outDir": "./dist",
|
"outDir": "dist",
|
||||||
// "removeComments": true, /* Disable emitting comments. */
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
@@ -110,6 +110,14 @@
|
|||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": [
|
||||||
"exclude": ["node_modules", "dist"]
|
"src/**/*.ts",
|
||||||
|
"integration/**/*.ts",
|
||||||
|
"*.ts",
|
||||||
|
"*.mjs"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user