feat: expose core identity transport
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
type CreateIdentityConfigOptions,
|
||||
createIdentityConfig,
|
||||
type IdentityConfig,
|
||||
type IdentityConfigInput,
|
||||
} from "@ekklesia/core/identity";
|
||||
|
||||
export type ApiConfigErrorCode =
|
||||
| "DATABASE_URL_INVALID"
|
||||
| "DATABASE_URL_MISSING"
|
||||
| "HOST_INVALID"
|
||||
| "PORT_INVALID";
|
||||
|
||||
/** Never carries the offending database URL, host, port, or any other caller-supplied value. */
|
||||
export class ApiConfigError extends Error {
|
||||
readonly code: ApiConfigErrorCode;
|
||||
|
||||
constructor(code: ApiConfigErrorCode) {
|
||||
super(apiConfigErrorMessages[code]);
|
||||
this.name = "ApiConfigError";
|
||||
this.code = code;
|
||||
this.stack = `${this.name}: ${this.message}`;
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
const apiConfigErrorMessages: Readonly<Record<ApiConfigErrorCode, string>> = Object.freeze({
|
||||
DATABASE_URL_INVALID:
|
||||
"The API database URL must be an absolute postgres:// or postgresql:// URL.",
|
||||
DATABASE_URL_MISSING: "The API database URL is required.",
|
||||
HOST_INVALID: "The API listen address must be a non-empty host.",
|
||||
PORT_INVALID:
|
||||
"The API listen port must be an integer between 0 and 65535 (0 requests an OS-assigned port).",
|
||||
});
|
||||
|
||||
const defaultHost = "127.0.0.1";
|
||||
const defaultPort = 3000;
|
||||
|
||||
export interface ApiRuntimeConfig {
|
||||
readonly databaseUrl: string;
|
||||
readonly host: string;
|
||||
readonly identity: IdentityConfig;
|
||||
readonly port: number;
|
||||
}
|
||||
|
||||
export interface ApiRuntimeConfigInput {
|
||||
readonly databaseUrl?: unknown;
|
||||
readonly host?: unknown;
|
||||
readonly identity: IdentityConfigInput;
|
||||
readonly port?: unknown;
|
||||
}
|
||||
|
||||
export interface CreateApiRuntimeConfigOptions {
|
||||
readonly environment?: CreateIdentityConfigOptions["environment"];
|
||||
}
|
||||
|
||||
function validateDatabaseUrl(databaseUrl: unknown): string {
|
||||
if (typeof databaseUrl !== "string" || databaseUrl.trim().length === 0) {
|
||||
throw new ApiConfigError("DATABASE_URL_MISSING");
|
||||
}
|
||||
let protocol: string;
|
||||
try {
|
||||
protocol = new URL(databaseUrl).protocol;
|
||||
} catch {
|
||||
throw new ApiConfigError("DATABASE_URL_INVALID");
|
||||
}
|
||||
if (protocol !== "postgres:" && protocol !== "postgresql:") {
|
||||
throw new ApiConfigError("DATABASE_URL_INVALID");
|
||||
}
|
||||
return databaseUrl;
|
||||
}
|
||||
|
||||
function validateHost(host: unknown): string {
|
||||
if (host === undefined) {
|
||||
return defaultHost;
|
||||
}
|
||||
if (typeof host !== "string" || host.trim().length === 0) {
|
||||
throw new ApiConfigError("HOST_INVALID");
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
function validatePort(port: unknown): number {
|
||||
if (port === undefined) {
|
||||
return defaultPort;
|
||||
}
|
||||
const numericPort = typeof port === "string" ? Number(port) : port;
|
||||
if (
|
||||
typeof numericPort !== "number" ||
|
||||
!Number.isInteger(numericPort) ||
|
||||
numericPort < 0 ||
|
||||
numericPort > 65_535
|
||||
) {
|
||||
throw new ApiConfigError("PORT_INVALID");
|
||||
}
|
||||
return numericPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates explicit, typed API runtime configuration, delegating identity fields to Core's
|
||||
* existing `createIdentityConfig` rather than defining a competing identity configuration
|
||||
* contract. Performs no environment reads, network calls, or database access.
|
||||
*/
|
||||
export function createApiRuntimeConfig(
|
||||
input: ApiRuntimeConfigInput,
|
||||
options: CreateApiRuntimeConfigOptions = {},
|
||||
): ApiRuntimeConfig {
|
||||
const databaseUrl = validateDatabaseUrl(input.databaseUrl);
|
||||
const host = validateHost(input.host);
|
||||
const port = validatePort(input.port);
|
||||
const identity = createIdentityConfig(
|
||||
input.identity,
|
||||
options.environment === undefined ? {} : { environment: options.environment },
|
||||
);
|
||||
|
||||
return Object.freeze({ databaseUrl, host, identity, port });
|
||||
}
|
||||
Reference in New Issue
Block a user