first commit

This commit is contained in:
2026-04-01 15:16:25 +02:00
commit a3a136c65d
9 changed files with 236 additions and 0 deletions

2
server/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { createPostalClient, createPostalClientFromEnv } from "./postal.js";
export type { CreatePostalClientOptions, PostalClient, PostalMessageInput } from "./postal.js";

79
server/postal.ts Normal file
View File

@@ -0,0 +1,79 @@
export type PostalMessageInput = {
to: string[];
subject: string;
plainBody: string;
htmlBody: string;
};
export type PostalClient = {
enabled: boolean;
sendMessage: (input: PostalMessageInput) => Promise<void>;
};
export type CreatePostalClientOptions = {
baseUrl: string;
apiKey: string;
from: string;
fetchImpl?: typeof fetch;
};
export type CreatePostalClientFromEnvOptions = {
baseUrlEnv?: string;
apiKeyEnv?: string;
fromEnv?: string;
env?: Record<string, string | undefined>;
fetchImpl?: typeof fetch;
};
export function createPostalClient(options: CreatePostalClientOptions): PostalClient {
const baseUrl = options.baseUrl.trim();
const apiKey = options.apiKey.trim();
const from = options.from.trim();
const fetchImpl = options.fetchImpl ?? fetch;
const enabled = baseUrl.length > 0 && apiKey.length > 0 && from.length > 0;
return {
enabled,
async sendMessage(input: PostalMessageInput): Promise<void> {
if (!enabled || input.to.length === 0) {
return;
}
const endpoint = new URL("/api/v1/send/message", ensureTrailingSlash(baseUrl));
const response = await fetchImpl(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Server-API-Key": apiKey
},
body: JSON.stringify({
from,
to: input.to,
subject: input.subject,
plain_body: input.plainBody,
html_body: input.htmlBody
})
});
const payload = (await response.json().catch(() => null)) as { status?: string } | null;
if (!response.ok || payload?.status !== "success") {
throw new Error(`postal_send_failed:${response.status}`);
}
}
};
}
export function createPostalClientFromEnv(options: CreatePostalClientFromEnvOptions = {}): PostalClient {
const env = options.env ?? process.env;
return createPostalClient({
baseUrl: env[options.baseUrlEnv ?? "POSTAL_URL"] ?? "",
apiKey: env[options.apiKeyEnv ?? "POSTAL_API_KEY"] ?? "",
from: env[options.fromEnv ?? "POSTAL_MESSAGE_FROM"] ?? "",
fetchImpl: options.fetchImpl
});
}
function ensureTrailingSlash(value: string): string {
return value.endsWith("/") ? value : `${value}/`;
}