feat: centralize mail credential and sandbox policy
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-23 21:51:37 +05:30
parent fe44bc2091
commit f52e1d50e4
4 changed files with 118 additions and 14 deletions
+85 -9
View File
@@ -1,4 +1,5 @@
import type { DefinedJob } from "@wrnexus/queue";
import { createKeyring, open, seal, type EncryptionKeyring } from "@wrnexus/encryption";
export interface MailMessage {
from?: string;
@@ -32,10 +33,12 @@ export function sealedMailDriver(options: SealedMailDriverOptions): MailDriver {
let cached: Promise<MailDriver> | undefined;
const create = async () => options.create(await options.unseal(options.sealedCredential));
const driver = () =>
options.cache === false ? create() : (cached ??= create().catch((error) => {
cached = undefined;
throw error;
}));
options.cache === false
? create()
: (cached ??= create().catch((error) => {
cached = undefined;
throw error;
}));
return {
async send(message) {
return (await driver()).send(message);
@@ -59,14 +62,87 @@ export interface MailOptions {
sandbox?: { enabled?: boolean; allowlist?: readonly string[] };
}
export function defineMail(options: MailOptions) {
export interface MailSandboxDecision {
allowed: boolean;
reason?: string;
}
/** Fail-closed, case-insensitive sandbox policy shared by mail workers and defineMail. */
export function mailSandboxDecision(
sandbox: { enabled?: boolean | number; allowlist?: readonly string[] | string },
recipient: unknown,
): MailSandboxDecision {
if (!sandbox.enabled) return { allowed: true };
if (typeof recipient !== "string" || !recipient.trim()) {
return {
allowed: false,
reason: "sandbox mode is on and no valid recipient address was given",
};
}
let source: unknown = sandbox.allowlist ?? [];
if (typeof source === "string") {
try {
source = JSON.parse(source || "[]");
} catch {
source = [];
}
}
const allowed = new Set(
(options.sandbox?.allowlist ?? []).map((value) => value.trim().toLowerCase()),
(Array.isArray(source) ? source : [])
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim().toLowerCase())
.filter(Boolean),
);
const normalized = recipient.trim().toLowerCase();
return allowed.has(normalized)
? { allowed: true }
: {
allowed: false,
reason: `sandbox mode is on and ${recipient} is not in the sandbox allow-list`,
};
}
export interface SealedMailCredentialsOptions {
/** Environment variable holding a base64 AES key. */
env?: string;
keyId?: string;
}
/** Owns validated, lazy encryption-key setup for credentials stored at rest. */
export function defineSealedMailCredentials(options: SealedMailCredentialsOptions = {}) {
const env = options.env ?? "APP_ENCRYPTION_KEY";
let cached: EncryptionKeyring | undefined;
const keyring = () => {
if (cached) return cached;
const secret = process.env[env];
if (!secret)
throw new Error(`${env} is not set; it is required to seal mail credentials at rest.`);
let length = 0;
try {
length = atob(secret).length;
} catch {
throw new Error(`${env} must be a valid base64-encoded 16, 24 or 32 byte key.`);
}
if (![16, 24, 32].includes(length)) {
throw new Error(
`${env} must decode to 16, 24 or 32 bytes; the current value decodes to ${length}.`,
);
}
return (cached = createKeyring([{ id: options.keyId ?? "primary", secret, active: true }]));
};
return {
keyring,
seal: (plain: string) => seal(plain, keyring()),
open: (sealed: string) => open(sealed, keyring()),
};
}
export function defineMail(options: MailOptions) {
const assertAllowed = (recipients: readonly string[]) => {
if (!options.sandbox?.enabled) return;
const denied = recipients.find((recipient) => !allowed.has(recipient.trim().toLowerCase()));
if (denied) throw new Error(`WRN-MAIL-SANDBOX: recipient '${denied}' is not allow-listed`);
for (const recipient of recipients) {
const decision = mailSandboxDecision(options.sandbox ?? {}, recipient);
if (!decision.allowed) throw new Error(`WRN-MAIL-SANDBOX: ${decision.reason}`);
}
};
return {
async send(message: MailMessage) {