diff --git a/.gitignore b/.gitignore index 497ef55..18745f6 100755 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ mtrace.json .env test.Dockerfile -node_modules/ \ No newline at end of file +node_modules/ +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/bun.lock b/bun.lock index f041cbc..e12e73c 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "godoxy-types", "devDependencies": { "prettier": "^3.4.2", + "typescript": "^5.7.3", "typescript-json-schema": "^0.65.1", }, }, @@ -94,7 +95,7 @@ "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], - "typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="], + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], "typescript-json-schema": ["typescript-json-schema@0.65.1", "", { "dependencies": { "@types/json-schema": "^7.0.9", "@types/node": "^18.11.9", "glob": "^7.1.7", "path-equal": "^1.2.5", "safe-stable-stringify": "^2.2.0", "ts-node": "^10.9.1", "typescript": "~5.5.0", "yargs": "^17.1.1" }, "bin": { "typescript-json-schema": "bin/typescript-json-schema" } }, "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg=="], @@ -113,5 +114,7 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + + "typescript-json-schema/typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="], } } diff --git a/package.json b/package.json index 8d6cf51..d53f2fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,8 @@ { "name": "godoxy-schemas", - "version": "0.9.0", + "version": "0.9.0-10", + "description": "JSON Schema and typescript types for GoDoxy configuration", + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/yusing/go-proxy" @@ -10,8 +12,18 @@ "README.md", "LICENSE" ], + "type": "module", + "main": "./schemas/index.ts", + "exports": { + ".": { + "types": "./schemas/index.d.ts", + "import": "./schemas/index.ts", + "require": "./schemas/index.js" + } + }, "devDependencies": { "prettier": "^3.4.2", + "typescript": "^5.7.3", "typescript-json-schema": "^0.65.1" }, "displayName": "GoDoxy Types", diff --git a/schemas/config/access_log.d.ts b/schemas/config/access_log.d.ts new file mode 100644 index 0000000..7d3c304 --- /dev/null +++ b/schemas/config/access_log.d.ts @@ -0,0 +1,49 @@ +import { CIDR, HTTPHeader, HTTPMethod, StatusCodeRange, URI } from "../types"; +export declare const ACCESS_LOG_FORMATS: readonly ["combined", "common", "json"]; +export type AccessLogFormat = (typeof ACCESS_LOG_FORMATS)[number]; +export type AccessLogConfig = { + /** + * The size of the buffer. + * + * @minimum 0 + * @default 65536 + * @TJS-type integer + */ + buffer_size?: number; + /** The format of the access log. + * + * @default "combined" + */ + format?: AccessLogFormat; + path: URI; + filters?: AccessLogFilters; + fields?: AccessLogFields; +}; +export type AccessLogFilter = { + /** Whether the filter is negative. + * + * @default false + */ + negative?: boolean; + values: T[]; +}; +export type AccessLogFilters = { + status_code?: AccessLogFilter; + method?: AccessLogFilter; + host?: AccessLogFilter; + headers?: AccessLogFilter; + cidr?: AccessLogFilter; +}; +export declare const ACCESS_LOG_FIELD_MODES: readonly ["keep", "drop", "redact"]; +export type AccessLogFieldMode = (typeof ACCESS_LOG_FIELD_MODES)[number]; +export type AccessLogField = { + default?: AccessLogFieldMode; + config: { + [key: string]: AccessLogFieldMode; + }; +}; +export type AccessLogFields = { + header?: AccessLogField; + query?: AccessLogField; + cookie?: AccessLogField; +}; diff --git a/schemas/config/access_log.js b/schemas/config/access_log.js new file mode 100644 index 0000000..0876f2a --- /dev/null +++ b/schemas/config/access_log.js @@ -0,0 +1,2 @@ +export const ACCESS_LOG_FORMATS = ["combined", "common", "json"]; +export const ACCESS_LOG_FIELD_MODES = ["keep", "drop", "redact"]; diff --git a/schemas/config/autocert.d.ts b/schemas/config/autocert.d.ts new file mode 100644 index 0000000..8577c8e --- /dev/null +++ b/schemas/config/autocert.d.ts @@ -0,0 +1,56 @@ +import { DomainOrWildcards as DomainsOrWildcards, Email } from "../types"; +export declare const AUTOCERT_PROVIDERS: readonly ["local", "cloudflare", "clouddns", "duckdns", "ovh"]; +export type AutocertProvider = (typeof AUTOCERT_PROVIDERS)[number]; +export type AutocertConfig = LocalOptions | CloudflareOptions | CloudDNSOptions | DuckDNSOptions | OVHOptionsWithAppKey | OVHOptionsWithOAuth2Config; +export interface AutocertConfigBase { + email: Email; + domains: DomainsOrWildcards; + cert_path?: string; + key_path?: string; +} +export interface LocalOptions extends AutocertConfigBase { + provider: "local"; +} +export interface CloudflareOptions extends AutocertConfigBase { + provider: "cloudflare"; + options: { + auth_token: string; + }; +} +export interface CloudDNSOptions extends AutocertConfigBase { + provider: "clouddns"; + options: { + client_id: string; + email: Email; + password: string; + }; +} +export interface DuckDNSOptions extends AutocertConfigBase { + provider: "duckdns"; + options: { + token: string; + }; +} +export declare const OVH_ENDPOINTS: readonly ["ovh-eu", "ovh-ca", "ovh-us", "kimsufi-eu", "kimsufi-ca", "soyoustart-eu", "soyoustart-ca"]; +export type OVHEndpoint = (typeof OVH_ENDPOINTS)[number]; +export interface OVHOptionsWithAppKey extends AutocertConfigBase { + provider: "ovh"; + options: { + application_secret: string; + consumer_key: string; + api_endpoint?: OVHEndpoint; + application_key: string; + }; +} +export interface OVHOptionsWithOAuth2Config extends AutocertConfigBase { + provider: "ovh"; + options: { + application_secret: string; + consumer_key: string; + api_endpoint?: OVHEndpoint; + oauth2_config: { + client_id: string; + client_secret: string; + }; + }; +} diff --git a/schemas/config/autocert.js b/schemas/config/autocert.js new file mode 100644 index 0000000..3cf9be4 --- /dev/null +++ b/schemas/config/autocert.js @@ -0,0 +1,16 @@ +export const AUTOCERT_PROVIDERS = [ + "local", + "cloudflare", + "clouddns", + "duckdns", + "ovh", +]; +export const OVH_ENDPOINTS = [ + "ovh-eu", + "ovh-ca", + "ovh-us", + "kimsufi-eu", + "kimsufi-ca", + "soyoustart-eu", + "soyoustart-ca", +]; diff --git a/schemas/config/config.d.ts b/schemas/config/config.d.ts new file mode 100644 index 0000000..30fd3a7 --- /dev/null +++ b/schemas/config/config.d.ts @@ -0,0 +1,54 @@ +import { DomainNames } from "../types"; +import { AutocertConfig } from "./autocert"; +import { EntrypointConfig } from "./entrypoint"; +import { HomepageConfig } from "./homepage"; +import { Providers } from "./providers"; +export type Config = { + /** Optional autocert configuration + * + * @examples require(".").autocertExamples + */ + autocert?: AutocertConfig; + entrypoint?: EntrypointConfig; + providers: Providers; + /** Optional list of domains to match + * + * @minItems 1 + * @examples require(".").matchDomainsExamples + */ + match_domains?: DomainNames; + homepage?: HomepageConfig; + /** + * Optional timeout before shutdown + * @default 3 + * @minimum 1 + */ + timeout_shutdown?: number; +}; +export declare const autocertExamples: ({ + provider: string; + email?: undefined; + domains?: undefined; + options?: undefined; +} | { + provider: string; + email: string; + domains: string[]; + options: { + auth_token: string; + client_id?: undefined; + email?: undefined; + password?: undefined; + }; +} | { + provider: string; + email: string; + domains: string[]; + options: { + client_id: string; + email: string; + password: string; + auth_token?: undefined; + }; +})[]; +export declare const matchDomainsExamples: readonly ["example.com", "*.example.com"]; diff --git a/schemas/config/config.js b/schemas/config/config.js new file mode 100644 index 0000000..56cc465 --- /dev/null +++ b/schemas/config/config.js @@ -0,0 +1,20 @@ +export const autocertExamples = [ + { provider: "local" }, + { + provider: "cloudflare", + email: "abc@gmail", + domains: ["example.com"], + options: { auth_token: "c1234565789-abcdefghijklmnopqrst" }, + }, + { + provider: "clouddns", + email: "abc@gmail", + domains: ["example.com"], + options: { + client_id: "c1234565789", + email: "abc@gmail", + password: "password", + }, + }, +]; +export const matchDomainsExamples = ["example.com", "*.example.com"]; diff --git a/schemas/config/entrypoint.d.ts b/schemas/config/entrypoint.d.ts new file mode 100644 index 0000000..3c2928c --- /dev/null +++ b/schemas/config/entrypoint.d.ts @@ -0,0 +1,39 @@ +import { MiddlewareCompose } from "../middlewares/middleware_compose"; +import { AccessLogConfig } from "./access_log"; +export type EntrypointConfig = { + /** Entrypoint middleware configuration + * + * @examples require(".").middlewaresExamples + */ + middlewares: MiddlewareCompose; + /** Entrypoint access log configuration + * + * @examples require(".").accessLogExamples + */ + access_log?: AccessLogConfig; +}; +export declare const accessLogExamples: readonly [{ + readonly path: "/var/log/access.log"; + readonly format: "combined"; + readonly filters: { + readonly status_codes: { + readonly values: readonly ["200-299"]; + }; + }; + readonly fields: { + readonly headers: { + readonly default: "keep"; + readonly config: { + readonly foo: "redact"; + }; + }; + }; +}]; +export declare const middlewaresExamples: readonly [{ + readonly use: "RedirectHTTP"; +}, { + readonly use: "CIDRWhitelist"; + readonly allow: readonly ["127.0.0.1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]; + readonly status: 403; + readonly message: "Forbidden"; +}]; diff --git a/schemas/config/entrypoint.js b/schemas/config/entrypoint.js new file mode 100644 index 0000000..d81c2ba --- /dev/null +++ b/schemas/config/entrypoint.js @@ -0,0 +1,30 @@ +export const accessLogExamples = [ + { + path: "/var/log/access.log", + format: "combined", + filters: { + status_codes: { + values: ["200-299"], + }, + }, + fields: { + headers: { + default: "keep", + config: { + foo: "redact", + }, + }, + }, + }, +]; +export const middlewaresExamples = [ + { + use: "RedirectHTTP", + }, + { + use: "CIDRWhitelist", + allow: ["127.0.0.1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], + status: 403, + message: "Forbidden", + }, +]; diff --git a/schemas/config/homepage.d.ts b/schemas/config/homepage.d.ts new file mode 100644 index 0000000..0ac498d --- /dev/null +++ b/schemas/config/homepage.d.ts @@ -0,0 +1,7 @@ +export type HomepageConfig = { + /** + * Use default app categories (uses docker image name) + * @default true + */ + use_default_categories: boolean; +}; diff --git a/schemas/config/homepage.js b/schemas/config/homepage.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/schemas/config/homepage.js @@ -0,0 +1 @@ +export {}; diff --git a/schemas/config/notification.d.ts b/schemas/config/notification.d.ts new file mode 100644 index 0000000..7646eb0 --- /dev/null +++ b/schemas/config/notification.d.ts @@ -0,0 +1,52 @@ +import { URL } from "../types"; +export declare const NOTIFICATION_PROVIDERS: readonly ["webhook", "gotify"]; +export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number]; +export type NotificationConfig = { + name: string; + url: URL; +}; +export interface GotifyConfig extends NotificationConfig { + provider: "gotify"; + token: string; +} +export declare const WEBHOOK_TEMPLATES: readonly ["discord"]; +export declare const WEBHOOK_METHODS: readonly ["POST", "GET", "PUT"]; +export declare const WEBHOOK_MIME_TYPES: readonly ["application/json", "application/x-www-form-urlencoded", "text/plain"]; +export declare const WEBHOOK_COLOR_MODES: readonly ["hex", "dec"]; +export type WebhookTemplate = (typeof WEBHOOK_TEMPLATES)[number]; +export type WebhookMethod = (typeof WEBHOOK_METHODS)[number]; +export type WebhookMimeType = (typeof WEBHOOK_MIME_TYPES)[number]; +export type WebhookColorMode = (typeof WEBHOOK_COLOR_MODES)[number]; +export interface WebhookConfig extends NotificationConfig { + provider: "webhook"; + /** + * Webhook template + * + * @default "discord" + */ + template?: WebhookTemplate; + token?: string; + /** + * Webhook message (usally JSON), + * required when template is not defined + */ + payload?: string; + /** + * Webhook method + * + * @default "POST" + */ + method?: WebhookMethod; + /** + * Webhook mime type + * + * @default "application/json" + */ + mime_type?: WebhookMimeType; + /** + * Webhook color mode + * + * @default "hex" + */ + color_mode?: WebhookColorMode; +} diff --git a/schemas/config/notification.js b/schemas/config/notification.js new file mode 100644 index 0000000..8ced351 --- /dev/null +++ b/schemas/config/notification.js @@ -0,0 +1,9 @@ +export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"]; +export const WEBHOOK_TEMPLATES = ["discord"]; +export const WEBHOOK_METHODS = ["POST", "GET", "PUT"]; +export const WEBHOOK_MIME_TYPES = [ + "application/json", + "application/x-www-form-urlencoded", + "text/plain", +]; +export const WEBHOOK_COLOR_MODES = ["hex", "dec"]; diff --git a/schemas/config/providers.d.ts b/schemas/config/providers.d.ts new file mode 100644 index 0000000..7cdb5ad --- /dev/null +++ b/schemas/config/providers.d.ts @@ -0,0 +1,44 @@ +import { URI, URL } from "../types"; +import { GotifyConfig, WebhookConfig } from "./notification"; +export type Providers = { + /** List of route definition files to include + * + * @minItems 1 + * @examples require(".").includeExamples + * @items.pattern ^[\w\d\-_]+\.(yaml|yml)$ + */ + include?: URI[]; + /** Name-value mapping of docker hosts to retrieve routes from + * + * @minProperties 1 + * @examples require(".").dockerExamples + */ + docker?: { + [name: string]: URL | "$DOCKER_HOST"; + }; + /** List of notification providers + * + * @minItems 1 + * @examples require(".").notificationExamples + */ + notification?: (WebhookConfig | GotifyConfig)[]; +}; +export declare const includeExamples: readonly ["file1.yml", "file2.yml"]; +export declare const dockerExamples: readonly [{ + readonly local: "$DOCKER_HOST"; +}, { + readonly remote: "tcp://10.0.2.1:2375"; +}, { + readonly remote2: "ssh://root:1234@10.0.2.2"; +}]; +export declare const notificationExamples: readonly [{ + readonly name: "gotify"; + readonly provider: "gotify"; + readonly url: "https://gotify.domain.tld"; + readonly token: "abcd"; +}, { + readonly name: "discord"; + readonly provider: "webhook"; + readonly template: "discord"; + readonly url: "https://discord.com/api/webhooks/1234/abcd"; +}]; diff --git a/schemas/config/providers.js b/schemas/config/providers.js new file mode 100644 index 0000000..1cc6fb4 --- /dev/null +++ b/schemas/config/providers.js @@ -0,0 +1,20 @@ +export const includeExamples = ["file1.yml", "file2.yml"]; +export const dockerExamples = [ + { local: "$DOCKER_HOST" }, + { remote: "tcp://10.0.2.1:2375" }, + { remote2: "ssh://root:1234@10.0.2.2" }, +]; +export const notificationExamples = [ + { + name: "gotify", + provider: "gotify", + url: "https://gotify.domain.tld", + token: "abcd", + }, + { + name: "discord", + provider: "webhook", + template: "discord", + url: "https://discord.com/api/webhooks/1234/abcd", + }, +]; diff --git a/schemas/docker.d.ts b/schemas/docker.d.ts new file mode 100644 index 0000000..3b3c06d --- /dev/null +++ b/schemas/docker.d.ts @@ -0,0 +1,5 @@ +import { IdleWatcherConfig } from "./providers/idlewatcher"; +import { Route } from "./providers/routes"; +export type DockerRoutes = { + [key: string]: Route & IdleWatcherConfig; +}; diff --git a/schemas/docker.js b/schemas/docker.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/schemas/docker.js @@ -0,0 +1 @@ +export {}; diff --git a/schemas/index.d.ts b/schemas/index.d.ts new file mode 100644 index 0000000..d1dfcc5 --- /dev/null +++ b/schemas/index.d.ts @@ -0,0 +1,15 @@ +import * as AccessLog from "./config/access_log"; +import * as Autocert from "./config/autocert"; +import * as Config from "./config/config"; +import * as Entrypoint from "./config/entrypoint"; +import * as Notification from "./config/notification"; +import * as Providers from "./config/providers"; +import * as MiddlewareCompose from "./middlewares/middleware_compose"; +import * as Middlewares from "./middlewares/middlewares"; +import * as Healthcheck from "./providers/healthcheck"; +import * as Homepage from "./providers/homepage"; +import * as IdleWatcher from "./providers/idlewatcher"; +import * as LoadBalance from "./providers/loadbalance"; +import * as Routes from "./providers/routes"; +import * as GoDoxy from "./types"; +export { AccessLog, Autocert, Config, Entrypoint, GoDoxy, Healthcheck, Homepage, IdleWatcher, LoadBalance, MiddlewareCompose, Middlewares, Notification, Providers, Routes, }; diff --git a/schemas/index.js b/schemas/index.js new file mode 100644 index 0000000..d1dfcc5 --- /dev/null +++ b/schemas/index.js @@ -0,0 +1,15 @@ +import * as AccessLog from "./config/access_log"; +import * as Autocert from "./config/autocert"; +import * as Config from "./config/config"; +import * as Entrypoint from "./config/entrypoint"; +import * as Notification from "./config/notification"; +import * as Providers from "./config/providers"; +import * as MiddlewareCompose from "./middlewares/middleware_compose"; +import * as Middlewares from "./middlewares/middlewares"; +import * as Healthcheck from "./providers/healthcheck"; +import * as Homepage from "./providers/homepage"; +import * as IdleWatcher from "./providers/idlewatcher"; +import * as LoadBalance from "./providers/loadbalance"; +import * as Routes from "./providers/routes"; +import * as GoDoxy from "./types"; +export { AccessLog, Autocert, Config, Entrypoint, GoDoxy, Healthcheck, Homepage, IdleWatcher, LoadBalance, MiddlewareCompose, Middlewares, Notification, Providers, Routes, }; diff --git a/schemas/index.ts b/schemas/index.ts new file mode 100644 index 0000000..5159ee0 --- /dev/null +++ b/schemas/index.ts @@ -0,0 +1,34 @@ +import * as AccessLog from "./config/access_log"; +import * as Autocert from "./config/autocert"; +import * as Config from "./config/config"; +import * as Entrypoint from "./config/entrypoint"; +import * as Notification from "./config/notification"; +import * as Providers from "./config/providers"; + +import * as MiddlewareCompose from "./middlewares/middleware_compose"; +import * as Middlewares from "./middlewares/middlewares"; + +import * as Healthcheck from "./providers/healthcheck"; +import * as Homepage from "./providers/homepage"; +import * as IdleWatcher from "./providers/idlewatcher"; +import * as LoadBalance from "./providers/loadbalance"; +import * as Routes from "./providers/routes"; + +import * as GoDoxy from "./types"; + +export { + AccessLog, + Autocert, + Config, + Entrypoint, + GoDoxy, + Healthcheck, + Homepage, + IdleWatcher, + LoadBalance, + MiddlewareCompose, + Middlewares, + Notification, + Providers, + Routes, +}; diff --git a/schemas/middlewares/middleware_compose.d.ts b/schemas/middlewares/middleware_compose.d.ts new file mode 100644 index 0000000..132a1b9 --- /dev/null +++ b/schemas/middlewares/middleware_compose.d.ts @@ -0,0 +1,2 @@ +import { MiddlewareComposeMap } from "./middlewares"; +export type MiddlewareCompose = MiddlewareComposeMap[]; diff --git a/schemas/middlewares/middleware_compose.js b/schemas/middlewares/middleware_compose.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/schemas/middlewares/middleware_compose.js @@ -0,0 +1 @@ +export {}; diff --git a/schemas/middlewares/middlewares.d.ts b/schemas/middlewares/middlewares.d.ts new file mode 100644 index 0000000..5444fe8 --- /dev/null +++ b/schemas/middlewares/middlewares.d.ts @@ -0,0 +1,123 @@ +import * as types from "../types"; +export type MiddlewareComposeObjectRef = `${string}@file`; +export type KeyOptMapping = { + [key in T["use"]]: Omit; +} | { + use: MiddlewareComposeObjectRef; +}; +export type MiddlewaresMap = KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | KeyOptMapping | { + [key in MiddlewareComposeObjectRef]: types.NullOrEmptyMap; +}; +export type MiddlewareComposeMap = CustomErrorPage | RedirectHTTP | SetXForwarded | HideXForwarded | CIDRWhitelist | CloudflareRealIP | ModifyRequest | ModifyResponse | OIDC | RateLimit | RealIP; +export type CustomErrorPage = { + use: "error_page" | "errorPage" | "ErrorPage" | "custom_error_page" | "customErrorPage" | "CustomErrorPage"; +}; +export type RedirectHTTP = { + use: "redirect_http" | "redirectHTTP" | "RedirectHTTP"; +}; +export type SetXForwarded = { + use: "set_x_forwarded" | "setXForwarded" | "SetXForwarded"; +}; +export type HideXForwarded = { + use: "hide_x_forwarded" | "hideXForwarded" | "HideXForwarded"; +}; +export type CIDRWhitelist = { + use: "cidr_whitelist" | "cidrWhitelist" | "CIDRWhitelist"; + allow: types.CIDR[]; + /** HTTP status code when blocked + * + * @default 403 + */ + status_code?: types.StatusCode; + /** HTTP status code when blocked (alias of status_code) + * + * @default 403 + */ + status?: types.StatusCode; + /** Error message when blocked + * + * @default "IP not allowed" + */ + message?: string; +}; +export type CloudflareRealIP = { + use: "cloudflare_real_ip" | "cloudflareRealIp" | "cloudflare_real_ip"; + /** Recursively resolve the IP + * + * @default false + */ + recursive?: boolean; +}; +export type ModifyRequest = { + use: "request" | "Request" | "modify_request" | "modifyRequest" | "ModifyRequest"; + /** Set HTTP headers */ + set_headers?: { + [key: types.HTTPHeader]: string; + }; + /** Add HTTP headers */ + add_headers?: { + [key: types.HTTPHeader]: string; + }; + /** Hide HTTP headers */ + hide_headers?: types.HTTPHeader[]; +}; +export type ModifyResponse = { + use: "response" | "Response" | "modify_response" | "modifyResponse" | "ModifyResponse"; + /** Set HTTP headers */ + set_headers?: { + [key: types.HTTPHeader]: string; + }; + /** Add HTTP headers */ + add_headers?: { + [key: types.HTTPHeader]: string; + }; + /** Hide HTTP headers */ + hide_headers?: types.HTTPHeader[]; +}; +export type OIDC = { + use: "oidc" | "OIDC"; + /** Allowed users + * + * @minItems 1 + */ + allowed_users?: string[]; + /** Allowed groups + * + * @minItems 1 + */ + allowed_groups?: string[]; +}; +export type RateLimit = { + use: "rate_limit" | "rateLimit" | "RateLimit"; + /** Average number of requests allowed in a period + * + * @min 1 + */ + average: number; + /** Maximum number of requests allowed in a period + * + * @min 1 + */ + burst: number; + /** Duration of the rate limit + * + * @default 1s + */ + period?: types.Duration; +}; +export type RealIP = { + use: "real_ip" | "realIP" | "RealIP"; + /** Header to get the client IP from + * + * @default "X-Real-IP" + */ + header?: types.HTTPHeader; + from: types.CIDR[]; + /** Recursive resolve the IP + * + * @default false + */ + recursive?: boolean; +}; diff --git a/schemas/middlewares/middlewares.js b/schemas/middlewares/middlewares.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/schemas/middlewares/middlewares.js @@ -0,0 +1 @@ +export {}; diff --git a/schemas/providers/healthcheck.d.ts b/schemas/providers/healthcheck.d.ts new file mode 100644 index 0000000..c819e54 --- /dev/null +++ b/schemas/providers/healthcheck.d.ts @@ -0,0 +1,32 @@ +import { Duration, URI } from "../types"; +/** + * @additionalProperties false + */ +export type HealthcheckConfig = { + /** Disable healthcheck + * + * @default false + */ + disable?: boolean; + /** Healthcheck path + * + * @default / + */ + path?: URI; + /** + * Use GET instead of HEAD + * + * @default false + */ + use_get?: boolean; + /** Healthcheck interval + * + * @default 5s + */ + interval?: Duration; + /** Healthcheck timeout + * + * @default 5s + */ + timeout?: Duration; +}; diff --git a/schemas/providers/healthcheck.js b/schemas/providers/healthcheck.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/schemas/providers/healthcheck.js @@ -0,0 +1 @@ +export {}; diff --git a/schemas/providers/homepage.d.ts b/schemas/providers/homepage.d.ts new file mode 100644 index 0000000..e0d66cf --- /dev/null +++ b/schemas/providers/homepage.d.ts @@ -0,0 +1,22 @@ +import { URL } from "../types"; +/** + * @additionalProperties false + */ +export type HomepageConfig = { + /** Whether show in dashboard + * + * @default true + */ + show?: boolean; + name?: string; + icon?: URL | WalkxcodeIcon | ExternalIcon | TargetRelativeIconPath; + description?: string; + url?: URL; + category?: string; + widget_config?: { + [key: string]: any; + }; +}; +export type WalkxcodeIcon = `${"png" | "svg" | "webp"}/${string}/${string}.${string}`; +export type ExternalIcon = `@${"selfhst" | "walkxcode"}/${string}.${string}`; +export type TargetRelativeIconPath = `@target/${string}` | `/${string}`; diff --git a/schemas/providers/homepage.js b/schemas/providers/homepage.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/schemas/providers/homepage.js @@ -0,0 +1 @@ +export {}; diff --git a/schemas/providers/idlewatcher.d.ts b/schemas/providers/idlewatcher.d.ts new file mode 100644 index 0000000..d9aed44 --- /dev/null +++ b/schemas/providers/idlewatcher.d.ts @@ -0,0 +1,25 @@ +import { Duration, URI } from "../types"; +export declare const STOP_METHODS: readonly ["pause", "stop", "kill"]; +export type StopMethod = (typeof STOP_METHODS)[number]; +export declare const STOP_SIGNALS: readonly ["", "SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT", "INT", "TERM", "HUP", "QUIT"]; +export type Signal = (typeof STOP_SIGNALS)[number]; +export type IdleWatcherConfig = { + idle_timeout?: Duration; + /** Wake timeout + * + * @default 30s + */ + wake_timeout?: Duration; + /** Stop timeout + * + * @default 30s + */ + stop_timeout?: Duration; + /** Stop method + * + * @default stop + */ + stop_method?: StopMethod; + stop_signal?: Signal; + start_endpoint?: URI; +}; diff --git a/schemas/providers/idlewatcher.js b/schemas/providers/idlewatcher.js new file mode 100644 index 0000000..3af0328 --- /dev/null +++ b/schemas/providers/idlewatcher.js @@ -0,0 +1,12 @@ +export const STOP_METHODS = ["pause", "stop", "kill"]; +export const STOP_SIGNALS = [ + "", + "SIGINT", + "SIGTERM", + "SIGHUP", + "SIGQUIT", + "INT", + "TERM", + "HUP", + "QUIT", +]; diff --git a/schemas/providers/loadbalance.d.ts b/schemas/providers/loadbalance.d.ts new file mode 100644 index 0000000..8ea4243 --- /dev/null +++ b/schemas/providers/loadbalance.d.ts @@ -0,0 +1,28 @@ +import { RealIP } from "../middlewares/middlewares"; +export declare const LOAD_BALANCE_MODES: readonly ["round_robin", "least_conn", "ip_hash"]; +export type LoadBalanceMode = (typeof LOAD_BALANCE_MODES)[number]; +export type LoadBalanceConfigBase = { + /** Alias (subdomain or FDN) of load-balancer + * + * @minLength 1 + */ + link: string; + /** Load-balance weight (reserved for future use) + * + * @minimum 0 + * @maximum 100 + */ + weight?: number; +}; +export type LoadBalanceConfig = LoadBalanceConfigBase & ({} | RoundRobinLoadBalanceConfig | LeastConnLoadBalanceConfig | IPHashLoadBalanceConfig); +export type IPHashLoadBalanceConfig = { + mode: "ip_hash"; + /** Real IP config, header to get client IP from */ + config: RealIP; +}; +export type LeastConnLoadBalanceConfig = { + mode: "least_conn"; +}; +export type RoundRobinLoadBalanceConfig = { + mode: "round_robin"; +}; diff --git a/schemas/providers/loadbalance.js b/schemas/providers/loadbalance.js new file mode 100644 index 0000000..da3887d --- /dev/null +++ b/schemas/providers/loadbalance.js @@ -0,0 +1,5 @@ +export const LOAD_BALANCE_MODES = [ + "round_robin", + "least_conn", + "ip_hash", +]; diff --git a/schemas/providers/routes.d.ts b/schemas/providers/routes.d.ts new file mode 100644 index 0000000..22bff43 --- /dev/null +++ b/schemas/providers/routes.d.ts @@ -0,0 +1,102 @@ +import { AccessLogConfig } from "../config/access_log"; +import { accessLogExamples } from "../config/entrypoint"; +import { MiddlewaresMap } from "../middlewares/middlewares"; +import { Hostname, IPv4, IPv6, PathPattern, Port, StreamPort } from "../types"; +import { HealthcheckConfig } from "./healthcheck"; +import { HomepageConfig } from "./homepage"; +import { LoadBalanceConfig } from "./loadbalance"; +export declare const PROXY_SCHEMES: readonly ["http", "https"]; +export declare const STREAM_SCHEMES: readonly ["tcp", "udp"]; +export type ProxyScheme = (typeof PROXY_SCHEMES)[number]; +export type StreamScheme = (typeof STREAM_SCHEMES)[number]; +export type Route = ReverseProxyRoute | StreamRoute; +export type Routes = { + [key: string]: Route; +}; +export type ReverseProxyRoute = { + /** Alias (subdomain or FDN) + * @minLength 1 + */ + alias?: string; + /** Proxy scheme + * + * @default http + */ + scheme?: ProxyScheme; + /** Proxy host + * + * @default localhost + */ + host?: Hostname | IPv4 | IPv6; + /** Proxy port + * + * @default 80 + */ + port?: Port; + /** Skip TLS verification + * + * @default false + */ + no_tls_verify?: boolean; + /** Path patterns (only patterns that match will be proxied). + * + * See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux + */ + path_patterns?: PathPattern[]; + /** Healthcheck config */ + healthcheck?: HealthcheckConfig; + /** Load balance config */ + load_balance?: LoadBalanceConfig; + /** Middlewares */ + middlewares?: MiddlewaresMap; + /** Homepage config + * + * @examples require(".").homepageExamples + */ + homepage?: HomepageConfig; + /** Access log config + * + * @examples require(".").accessLogExamples + */ + access_log?: AccessLogConfig; +}; +export type StreamRoute = { + /** Alias (subdomain or FDN) + * @minLength 1 + */ + alias?: string; + /** Stream scheme + * + * @default tcp + */ + scheme: StreamScheme; + /** Stream host + * + * @default localhost + */ + host?: Hostname | IPv4 | IPv6; + port: StreamPort; + /** Healthcheck config */ + healthcheck?: HealthcheckConfig; +}; +export declare const homepageExamples: ({ + name: string; + icon: string; + category: string; +} | { + name: string; + icon: string; + category?: undefined; +})[]; +export declare const loadBalanceExamples: ({ + link: string; + mode: string; + config?: undefined; +} | { + link: string; + mode: string; + config: { + header: string; + }; +})[]; +export { accessLogExamples }; diff --git a/schemas/providers/routes.js b/schemas/providers/routes.js new file mode 100644 index 0000000..931e113 --- /dev/null +++ b/schemas/providers/routes.js @@ -0,0 +1,28 @@ +import { accessLogExamples } from "../config/entrypoint"; +export const PROXY_SCHEMES = ["http", "https"]; +export const STREAM_SCHEMES = ["tcp", "udp"]; +export const homepageExamples = [ + { + name: "Sonarr", + icon: "png/sonarr.png", + category: "Arr suite", + }, + { + name: "App", + icon: "@target/favicon.ico", + }, +]; +export const loadBalanceExamples = [ + { + link: "flaresolverr", + mode: "round_robin", + }, + { + link: "service.domain.com", + mode: "ip_hash", + config: { + header: "X-Real-IP", + }, + }, +]; +export { accessLogExamples }; diff --git a/schemas/types.d.ts b/schemas/types.d.ts new file mode 100644 index 0000000..b2f5222 --- /dev/null +++ b/schemas/types.d.ts @@ -0,0 +1,94 @@ +/** + * @type "null" + */ +export interface Null { +} +export type Nullable = T | Null; +export type NullOrEmptyMap = {} | Null; +export declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "HEAD", "OPTIONS", "TRACE"]; +export type HTTPMethod = (typeof HTTP_METHODS)[number]; +/** + * HTTP Header + * @pattern ^[a-zA-Z0-9\-]+$ + * @type string + */ +export type HTTPHeader = string & {}; +/** + * HTTP Query + * @pattern ^[a-zA-Z0-9\-_]+$ + * @type string + */ +export type HTTPQuery = string & {}; +/** + * HTTP Cookie + * @pattern ^[a-zA-Z0-9\-_]+$ + * @type string + */ +export type HTTPCookie = string & {}; +export type StatusCode = number | `${number}`; +export type StatusCodeRange = number | `${number}` | `${number}-${number}`; +/** + * @items.pattern ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + */ +export type DomainNames = string[]; +/** + * @items.pattern ^(\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + */ +export type DomainOrWildcards = string[]; +/** + * @format hostname + * @type string + */ +export type Hostname = string & {}; +/** + * @format ipv4 + * @type string + */ +export type IPv4 = string & {}; +/** + * @format ipv6 + * @type string + */ +export type IPv6 = string & {}; +export type CIDR = `${number}.${number}.${number}.${number}` | `${string}:${string}:${string}:${string}:${string}:${string}:${string}:${string}` | `${number}.${number}.${number}.${number}/${number}` | `::${number}` | `${string}::/${number}` | `${string}:${string}::/${number}`; +/** + * @type integer + * @minimum 0 + * @maximum 65535 + */ +export type Port = number | `${number}`; +/** + * @pattern ^\d+:\d+$ + * @type string + */ +export type StreamPort = string & {}; +/** + * @format email + * @type string + */ +export type Email = string & {}; +/** + * @format uri + * @type string + */ +export type URL = string & {}; +/** + * @format uri-reference + * @type string + */ +export type URI = string & {}; +/** + * @pattern ^(?:([A-Z]+) )?(?:([a-zA-Z0-9.-]+)\\/)?(\\/[^\\s]*)$ + * @type string + */ +export type PathPattern = string & {}; +/** + * @pattern ^([0-9]+(ms|s|m|h))+$ + * @type string + */ +export type Duration = string & {}; +/** + * @format date-time + * @type string + */ +export type DateTime = string & {}; diff --git a/schemas/types.js b/schemas/types.js new file mode 100644 index 0000000..0f22981 --- /dev/null +++ b/schemas/types.js @@ -0,0 +1,11 @@ +export const HTTP_METHODS = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE", +]; diff --git a/tsconfig.json b/tsconfig.json index d8899d4..d355b74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,16 @@ { "compilerOptions": { + "incremental": true, "skipLibCheck": true, "target": "ESNext", "module": "ESNext", "moduleResolution": "Node", - "strict": true + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "resolveJsonModule": true, + "declaration": true }, - "include": ["schemas/**/*.ts"] + "include": ["schemas"] }