fix: close durable queue and runtime gaps
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mail",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -14,6 +14,39 @@ export interface MailDriver {
|
||||
test?(): Promise<{ ok: boolean; error?: string }>;
|
||||
}
|
||||
|
||||
export interface SealedMailDriverOptions {
|
||||
/** Ciphertext stored by the application; plaintext is never retained here. */
|
||||
sealedCredential: string;
|
||||
unseal(value: string): Promise<string>;
|
||||
create(credential: string): MailDriver | Promise<MailDriver>;
|
||||
/** Cache the initialized driver. Defaults to true. */
|
||||
cache?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily opens a sealed credential only at the transport boundary. This keeps
|
||||
* encryption policy and SMTP/vendor choice independent while preventing every
|
||||
* app from rebuilding the same decrypt-on-send lifecycle.
|
||||
*/
|
||||
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;
|
||||
}));
|
||||
return {
|
||||
async send(message) {
|
||||
return (await driver()).send(message);
|
||||
},
|
||||
async test() {
|
||||
const resolved = await driver();
|
||||
return resolved.test?.() ?? { ok: true };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface MailTemplate<T = Record<string, unknown>> {
|
||||
subject(data: T): string;
|
||||
html?(data: T): string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { defineMail, defineMailTemplate } from "../src/index.ts";
|
||||
import { defineMail, defineMailTemplate, sealedMailDriver } from "../src/index.ts";
|
||||
|
||||
test("mail templates render and sandbox blocks accidental recipients", async () => {
|
||||
const sent: unknown[] = [];
|
||||
@@ -16,3 +16,32 @@ test("mail templates render and sandbox blocks accidental recipients", async ()
|
||||
expect(sent).toHaveLength(1);
|
||||
await expect(mail.send({ to: "real@example.com", subject: "no" })).rejects.toThrow("SANDBOX");
|
||||
});
|
||||
|
||||
test("sealed drivers decrypt lazily once and delegate send/test", async () => {
|
||||
let opens = 0;
|
||||
const sent: string[] = [];
|
||||
const driver = sealedMailDriver({
|
||||
sealedCredential: "ciphertext",
|
||||
async unseal(value) {
|
||||
opens++;
|
||||
expect(value).toBe("ciphertext");
|
||||
return "plaintext-secret";
|
||||
},
|
||||
create(credential) {
|
||||
expect(credential).toBe("plaintext-secret");
|
||||
return {
|
||||
async send(message) {
|
||||
sent.push(message.subject);
|
||||
},
|
||||
async test() {
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(opens).toBe(0);
|
||||
await driver.send({ to: "safe@example.test", subject: "one" });
|
||||
expect(await driver.test?.()).toEqual({ ok: true });
|
||||
expect(opens).toBe(1);
|
||||
expect(sent).toEqual(["one"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user