mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: use no-reply sender for staff emails (#2602)
This commit is contained in:
@@ -26,3 +26,4 @@ OPENAI_API_KEY=
|
||||
RESEND_API_KEY=
|
||||
CLAWHUB_SECURITY_EMAIL=security@notifications.openclaw.ai
|
||||
CLAWHUB_SECURITY_EMAIL_FROM=ClawHub Security <noreply@notifications.openclaw.ai>
|
||||
CLAWHUB_NOREPLY_FROM=ClawHub <noreply@notifications.openclaw.ai>
|
||||
|
||||
@@ -6954,6 +6954,103 @@ describe("httpApiV1 handlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("staff email uses the configured ClawHub noreply sender", async () => {
|
||||
vi.stubEnv("RESEND_API_KEY", "resend_test");
|
||||
vi.stubEnv("CLAWHUB_NOREPLY_FROM", "ClawHub <noreply@notifications.openclaw.ai>");
|
||||
vi.stubEnv("NOREPLY_EMAIL_FROM", "Legacy <legacy@example.com>");
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
||||
userId: "users:admin",
|
||||
user: { _id: "users:admin", role: "admin" },
|
||||
} as never);
|
||||
const runQuery = vi.fn().mockResolvedValueOnce({
|
||||
_id: "users:recipient",
|
||||
handle: "demo",
|
||||
email: "demo@example.com",
|
||||
});
|
||||
const runMutation = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(okRate())
|
||||
.mockResolvedValueOnce({
|
||||
auditLogId: "auditLogs:staff-email",
|
||||
})
|
||||
.mockResolvedValueOnce({ ok: true });
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ id: "email_123" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await __handlers.usersPostRouterV1Handler(
|
||||
makeCtx({ runQuery, runMutation }),
|
||||
new Request("https://example.com/api/v1/users/email", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
userHandle: "demo",
|
||||
subject: "Notice",
|
||||
body: "Hello",
|
||||
confirmUserRequest: true,
|
||||
confirmUserSignoff: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
||||
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
|
||||
expect(resendBody.from).not.toBe("Legacy <legacy@example.com>");
|
||||
});
|
||||
|
||||
it("staff email ignores legacy noreply sender env names", async () => {
|
||||
vi.stubEnv("RESEND_API_KEY", "resend_test");
|
||||
vi.stubEnv("CLAWHUB_NOREPLY_FROM", "");
|
||||
vi.stubEnv("NOREPLY_EMAIL_FROM", "Legacy <legacy@example.com>");
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
||||
userId: "users:admin",
|
||||
user: { _id: "users:admin", role: "admin" },
|
||||
} as never);
|
||||
const runQuery = vi.fn().mockResolvedValueOnce({
|
||||
_id: "users:recipient",
|
||||
handle: "demo",
|
||||
email: "demo@example.com",
|
||||
});
|
||||
const runMutation = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(okRate())
|
||||
.mockResolvedValueOnce({
|
||||
auditLogId: "auditLogs:staff-email",
|
||||
})
|
||||
.mockResolvedValueOnce({ ok: true });
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ id: "email_123" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await __handlers.usersPostRouterV1Handler(
|
||||
makeCtx({ runQuery, runMutation }),
|
||||
new Request("https://example.com/api/v1/users/email", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
userHandle: "demo",
|
||||
subject: "Notice",
|
||||
body: "Hello",
|
||||
confirmUserRequest: true,
|
||||
confirmUserSignoff: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
||||
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
|
||||
});
|
||||
|
||||
it("set role requires auth", async () => {
|
||||
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error("Unauthorized"));
|
||||
const runMutation = vi.fn().mockResolvedValue(okRate());
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
toOptionalNumber,
|
||||
} from "./shared";
|
||||
|
||||
const DEFAULT_CLAWHUB_NOREPLY_FROM = "ClawHub <noreply@notifications.openclaw.ai>";
|
||||
|
||||
const usersV1InternalRefs = internal as unknown as {
|
||||
publishers: {
|
||||
addOfficialPublisherInternal: unknown;
|
||||
@@ -356,10 +358,7 @@ async function handleAdminStaffEmail(
|
||||
|
||||
const apiKey = process.env.RESEND_API_KEY?.trim();
|
||||
if (!apiKey) return text("RESEND_API_KEY is not configured", 500, headers);
|
||||
const from =
|
||||
process.env.CLAWHUB_NOREPLY_FROM?.trim() ||
|
||||
process.env.NOREPLY_EMAIL_FROM?.trim() ||
|
||||
"ClawHub <noreply@clawhub.ai>";
|
||||
const from = process.env.CLAWHUB_NOREPLY_FROM?.trim() || DEFAULT_CLAWHUB_NOREPLY_FROM;
|
||||
|
||||
const emailAudit = await runUsersV1MutationRef<{ auditLogId: Id<"auditLogs"> }>(
|
||||
ctx,
|
||||
|
||||
@@ -108,6 +108,8 @@ Ensure Convex env is set (auth + embeddings):
|
||||
- `CLAWHUB_SECURITY_EMAIL_FROM` for the outbound From header, defaulting to
|
||||
`ClawHub Security <noreply@notifications.openclaw.ai>` on the verified Resend
|
||||
domain
|
||||
- `CLAWHUB_NOREPLY_FROM` for guarded staff emails, defaulting to
|
||||
`ClawHub <noreply@notifications.openclaw.ai>` on the verified Resend domain
|
||||
- `SITE_URL` (your web app URL)
|
||||
- Optional webhook env (see `docs/webhook.md`)
|
||||
- Recommended GitHub App env for authenticated GitHub API reads used by publish
|
||||
|
||||
Reference in New Issue
Block a user