Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
22 changes: 21 additions & 1 deletion apps/backend/auth/src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { seedRuntimeSignupLockFromEnvFile } from "@reloop/auth/setup/setup-mode";
import { unlink } from "node:fs/promises";
import {
seedAdminSetupKeyFile,
seedRuntimeSignupLockFromEnvFile,
} from "@reloop/auth/setup/setup-mode";
import { bus } from "@reloop/bus";
import { RedisCache } from "@reloop/cache/redis-client";
import { db } from "@reloop/db/client";
Expand All @@ -8,6 +12,22 @@ import { authConfig } from "./auth.config";

const redis = new RedisCache("auth");
export const loader = async () => {
await unlink(`${authConfig.ADMIN_SETUP_KEY_FILE}.lock`).catch(() => {});
try {
const seed = authConfig.ADMIN_SETUP_KEY?.trim();
if (
seed &&
(await seedAdminSetupKeyFile(authConfig.ADMIN_SETUP_KEY_FILE, seed))
) {
log.info(
"server",
"Administrator setup key written from ADMIN_SETUP_KEY",
);
}
} catch (e) {
log.error({ message: String(e) });
}

try {
await redis.healthCheck();
log.info("server", "Redis connected");
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/mail/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createSessionCacheRedis,
resolveApiKeyInternalOrSession,
} from "@reloop/auth/middleware";
import { resolveInternalBaseUrl } from "@reloop/auth/middleware/internal-base-url";
import { Elysia } from "elysia";
import { evlog } from "evlog/elysia";
import { mailConfig } from "../mail.config";
Expand All @@ -30,7 +31,7 @@ export const authMiddleware = new Elysia({ name: "auth-middleware" })
auth: {
async resolve({ status, request: { headers } }) {
const ctx = await resolveApiKeyInternalOrSession(headers, {
baseUrl: mailConfig.BASE_URL,
baseUrl: resolveInternalBaseUrl(mailConfig.BASE_URL),
redis: sessionRedis,
ttl: 5,
internalSecret: mailConfig.RELOOP_INTERNAL_SECRET,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { extractApiKey } from "@reloop/auth/middleware";
import { mailConfig } from "@reloop/be-mail/mail.config";
import { authMiddleware } from "@reloop/be-mail/middleware/auth";
import { checkRateLimit } from "@reloop/be-mail/middleware/rate-limiter";
Expand Down Expand Up @@ -45,7 +46,7 @@ export const resendEmailRoute = new Elysia()
throw error;
}

const requestApiKey = request.headers.get("x-api-key");
const requestApiKey = extractApiKey(request.headers);
const useInternalInject = authType !== "apikey";
const injectApiKey = useInternalInject
? mailConfig.RELOOP_INTERNAL_SECRET
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { extractApiKey } from "@reloop/auth/middleware";
import { mailConfig } from "@reloop/be-mail/mail.config";
import { authMiddleware } from "@reloop/be-mail/middleware/auth";
import { checkRateLimit } from "@reloop/be-mail/middleware/rate-limiter";
Expand Down Expand Up @@ -47,7 +48,7 @@ export const sendEmailRoute = new Elysia()
throw error;
}

const requestApiKey = request.headers.get("x-api-key");
const requestApiKey = extractApiKey(request.headers);
// External API key path uses the caller's key for KumoMTA.
// Session/internal auth uses the shared internal secret so inject
// can authenticate without a recoverable plaintext org API key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function evaluateSpoofability(
verdict = "protected";
spoofable = false;
inboxOutcome = "rejected";
headline = `No — receivers are told to reject fakes as you@${domain}`;
headline = `No, receivers are told to reject fakes as you@${domain}`;
summary =
"Your domain enforces strict DMARC ('p=reject') and SPF protection. Major mailbox providers like Gmail, Yahoo, and Outlook are instructed to immediately discard fraudulent emails.";
reasons.push({
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/tools/test/spoof-checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe("Spoof Checker Service (evaluateSpoofability pure logic)", () => {
expect(res.spoofable).toBe(false);
expect(res.inboxOutcome).toBe("rejected");
expect(res.headline).toContain(
"No — receivers are told to reject fakes as you@stripe.com",
"No, receivers are told to reject fakes as you@stripe.com",
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default function AuditPage() {
<PageFrame>
<PageHeading
title="Audit log"
description="Every privileged console action — who did what, on which resource, and why when a reason was provided."
description="Every privileged console action: who did what, on which resource, and why when a reason was provided."
meta={
<span className="rounded-full bg-bg-weak-50 px-2.5 py-1 font-medium text-[12px] text-text-sub-600 tabular-nums dark:bg-white/[0.06]">
{data?.total ?? 0} total
Expand Down Expand Up @@ -138,7 +138,7 @@ export default function AuditPage() {
{item.actorName || truncateId(item.actorUserId, 12)}
</p>
<p className="text-[12px] text-text-sub-600">
{item.actorEmail || "—"}
{item.actorEmail || "-"}
</p>
</td>
<td className="px-4 py-3">
Expand All @@ -151,7 +151,7 @@ export default function AuditPage() {
<StatusPill status={item.resourceType} tone="blue" />
</div>
<p className="mt-1 font-mono text-[11px] text-text-sub-600">
{item.resourceId ? truncateId(item.resourceId, 16) : "—"}
{item.resourceId ? truncateId(item.resourceId, 16) : "-"}
</p>
</td>
<td className="px-4 py-3">
Expand All @@ -167,7 +167,7 @@ export default function AuditPage() {
</Link>
</Button.Root>
) : (
<span className="text-text-soft-400">—</span>
<span className="text-text-soft-400">-</span>
)}
</td>
<td className="max-w-[240px] px-4 py-3 text-[12px] text-text-sub-600">
Expand All @@ -176,7 +176,7 @@ export default function AuditPage() {
{JSON.stringify(item.metadata, null, 0)}
</pre>
) : (
"—"
"-"
)}
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default function CreditsPage() {
)
}
title="Credits ledger"
description="Balances and top-ups for a single organization. From day to day, open the org hub — this page is the deep-link ledger."
description="Balances and top-ups for a single organization. From day to day, open the org hub. This page is the deep-link ledger."
actions={
organizationId ? (
<>
Expand Down Expand Up @@ -325,7 +325,7 @@ export default function CreditsPage() {
{formatNumber(entry.balanceAfter)}
</td>
<td className="px-4 py-3 text-text-sub-600">
{entry.reason || "—"}
{entry.reason || "-"}
</td>
</tr>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function formatHtml(html: string): string {
}

function formatLongDate(value: string | Date | null | undefined) {
if (!value) return "—";
if (!value) return "-";
return new Date(value).toLocaleString(undefined, {
weekday: "long",
year: "numeric",
Expand Down Expand Up @@ -726,7 +726,7 @@ export default function EmailDetailPage() {
</div>

<div className="space-y-6 pb-16">
{/* Metadata — dashboard-style key/value rows */}
{/* Metadata: dashboard-style key/value rows */}
<section>
<div className="flex flex-col gap-3.5">
<MetaRow label="From">
Expand Down Expand Up @@ -818,7 +818,7 @@ export default function EmailDetailPage() {
{isLoading ? (
<Skeleton className="h-4 w-24 rounded-md" />
) : (
<span className="capitalize">{email?.provider || "—"}</span>
<span className="capitalize">{email?.provider || "-"}</span>
)}
</MetaRow>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function AttachmentCell({
attachments?: EmailItem["attachments"];
}) {
if (!attachments || attachments.length === 0) {
return <span className="text-[13px] text-text-soft-400">—</span>;
return <span className="text-[13px] text-text-soft-400">-</span>;
}

const first = attachments[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export default function InboundPage() {
</div>
</td>
<td className="max-w-[160px] truncate px-4 py-3 text-text-sub-600">
{email.mailboxEmail || "—"}
{email.mailboxEmail || "-"}
</td>
<td className="px-4 py-3">
<Button.Root
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,11 +581,11 @@ export default function OrganizationDetailPage() {
>
<p className={`text-[12px] font-semibold ${atCap.length ? "text-orange-700 dark:text-orange-300" : "text-amber-700 dark:text-amber-300"}`}>
{atCap.length
? `${atCap.length} domain${atCap.length > 1 ? "s" : ""} at daily limit — new sends are paused until UTC midnight`
: `${warming.length} domain${warming.length > 1 ? "s are" : " is"} warming up — daily sends are throttled for the first 30 days`}
? `${atCap.length} domain${atCap.length > 1 ? "s" : ""} at daily limit. New sends are paused until UTC midnight`
: `${warming.length} domain${warming.length > 1 ? "s are" : " is"} warming up. Daily sends are throttled for the first 30 days`}
</p>
<p className="mt-1 text-[11px] leading-relaxed text-text-sub-600 dark:text-white/60">
Applies to <span className="font-medium text-text-strong-950 dark:text-white">all plans (Free / Pro / Growth / Enterprise)</span> to protect reputation and block scam bulk sends from cheap, newly bought domains. Caps rise automatically as the domain ages — no manual action.
Applies to <span className="font-medium text-text-strong-950 dark:text-white">all plans (Free / Pro / Growth / Enterprise)</span> to protect reputation and block scam bulk sends from cheap, newly bought domains. Caps rise automatically as the domain ages. No manual action needed.
</p>
<div className="mt-3 space-y-2.5">
{warming.map((d) => {
Expand All @@ -606,7 +606,7 @@ export default function OrganizationDetailPage() {
</div>
<div className="mt-1.5 flex flex-wrap justify-between gap-2 text-[11px] leading-relaxed">
<span className={isAt ? "font-medium text-orange-700 dark:text-orange-300" : "text-text-sub-600 dark:text-white/60"}>
{isAt ? `At cap — 0 left today` : `${remaining} left today`}
{isAt ? `At cap, 0 left today` : `${remaining} left today`}
{d.dailyCap !== null && d.ageDays <= 1 ? " · send only to highly engaged / verified contacts" : ""}
</span>
{next && (
Expand All @@ -622,7 +622,7 @@ export default function OrganizationDetailPage() {
<div className="mt-3 rounded-lg bg-white/60 px-3 py-2.5 text-[11px] leading-relaxed text-text-sub-600 dark:bg-black/20 dark:text-white/60">
<p className="font-medium text-text-strong-950 dark:text-white">How caps work</p>
<p className="mt-1">
<span className="font-mono">0–1d: 20</span> (highly engaged only) → <span className="font-mono">2–3d: 50</span> → <span className="font-mono">4–7d: 100</span> → <span className="font-mono">8–14d: 250</span> → <span className="font-mono">15–30d: 500</span> → <span className="font-mono">30d+: Dynamic</span> (reputation-based; Free still max 100/day). Caps are per-domain per UTC day — resets at 00:00 UTC. If you hit the cap you’ll get <span className="font-mono">429 New domain daily limit reached</span>.
<span className="font-mono">0–1d: 20</span> (highly engaged only) → <span className="font-mono">2–3d: 50</span> → <span className="font-mono">4–7d: 100</span> → <span className="font-mono">8–14d: 250</span> → <span className="font-mono">15–30d: 500</span> → <span className="font-mono">30d+: Dynamic</span> (reputation-based; Free still max 100/day). Caps are per-domain per UTC day and reset at 00:00 UTC. If you hit the cap you’ll get <span className="font-mono">429 New domain daily limit reached</span>.
</p>
</div>
</div>
Expand All @@ -641,9 +641,9 @@ export default function OrganizationDetailPage() {
? `${planLabel(data.plan.planId)} · ${data.plan.monthlyEmails.toLocaleString()}/mo`
: "Free · 3,000/mo",
],
["Billing email", data.billingEmail || "—"],
["Billing name", data.billingName || "—"],
["External customer", data.externalCustomerId || "—"],
["Billing email", data.billingEmail || "-"],
["Billing name", data.billingName || "-"],
["External customer", data.externalCustomerId || "-"],
["Org ID", data.id],
["Created", formatDateTime(data.createdAt)],
["Updated", formatDateTime(data.updatedAt)],
Expand Down Expand Up @@ -749,7 +749,7 @@ export default function OrganizationDetailPage() {
{data.counts.mailboxes > 0 && data.mailboxes.length > 0 ? (
<div className="border-t border-stroke-soft-100 bg-bg-weak-50/30 px-4 py-3 dark:border-stroke-soft-100/40 dark:bg-white/[0.02]">
<p className="mb-2 text-[11px] font-medium uppercase tracking-wide text-text-sub-600">
Used inboxes — {data.mailboxes.length} {data.mailboxes.length === 1 ? "inbox" : "inboxes"}
Used inboxes: {data.mailboxes.length} {data.mailboxes.length === 1 ? "inbox" : "inboxes"}
</p>
<div className="space-y-2">
{data.mailboxes.map((mb) => (
Expand All @@ -761,7 +761,7 @@ export default function OrganizationDetailPage() {
<p className="truncate font-medium text-[12px] text-text-strong-950">{mb.email}</p>
<p className="truncate text-[11px] text-text-sub-600">
{mb.displayName ? `${mb.displayName} · ` : ""}
{mb.domain || "—"} · {formatDateTime(mb.createdAt)}
{mb.domain || "-"} · {formatDateTime(mb.createdAt)}
</p>
</div>
<StatusPill status={mb.status} />
Expand All @@ -771,7 +771,7 @@ export default function OrganizationDetailPage() {
</div>
) : data.counts.mailboxes === 0 ? (
<div className="border-t border-stroke-soft-100 px-4 py-3 text-[12px] text-text-sub-600 dark:border-stroke-soft-100/40">
No agent inboxes yet — {data.plan ? `${data.plan.maxAgentInboxes} available` : "1 available"} on {planLabel(data.plan?.planId)}
No agent inboxes yet ({data.plan ? `${data.plan.maxAgentInboxes} available` : "1 available"} on {planLabel(data.plan?.planId)})
</div>
) : null}
<div className="flex items-center justify-between gap-3 px-4 py-3 text-[12px]">
Expand Down Expand Up @@ -960,7 +960,7 @@ export default function OrganizationDetailPage() {
{tab === "domains" ? (
<SectionCard
title={`Domains (${data.domains.length})`}
description="Sending domains attached to this org — age cap applies to all plans"
description="Sending domains attached to this org (age cap applies to all plans)"
>
<DataTable
headers={["Domain", "Status", "Age", "Daily cap", "Sent today", "Verified", "Created"]}
Expand Down Expand Up @@ -1049,7 +1049,7 @@ export default function OrganizationDetailPage() {
key={k.id}
className="border-stroke-soft-100 border-t dark:border-stroke-soft-100/40"
>
<td className="px-4 py-3 font-medium">{k.name || "—"}</td>
<td className="px-4 py-3 font-medium">{k.name || "-"}</td>
<td className="px-4 py-3 font-mono text-[12px] text-text-sub-600">
{k.prefix || k.start || truncateId(k.id, 12)}
</td>
Expand Down Expand Up @@ -1103,10 +1103,10 @@ export default function OrganizationDetailPage() {
<StatusPill status={t.status} />
</td>
<td className="max-w-[220px] truncate px-4 py-3 text-text-sub-600">
{t.subject || "—"}
{t.subject || "-"}
</td>
<td className="px-4 py-3 text-text-sub-600">
{t.fromEmail || "—"}
{t.fromEmail || "-"}
</td>
<td className="px-4 py-3 tabular-nums">v{t.currentVersion}</td>
<td className="px-4 py-3 text-text-sub-600">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type OrgSendingIps = {
type InventoryResponse = { items: SendingIpItem[]; total: number };

function warmupLabel(warmup: WarmupView | null): string {
if (!warmup) return "—";
if (!warmup) return "-";
if (warmup.status === "completed") return "Warmed";
if (warmup.status === "paused") return "Paused";
if (warmup.status === "pending") return "Scheduled";
Expand Down Expand Up @@ -85,7 +85,7 @@ export function OrgSendingIps({
return (
<SectionCard
title="Dedicated IPs"
description={`Plan includes ${data?.dedicatedIpCount ?? "—"} · ${data?.assignedCount ?? 0} assigned. Register addresses on Sending IPs, then assign here.`}
description={`Plan includes ${data?.dedicatedIpCount ?? "-"} · ${data?.assignedCount ?? 0} assigned. Register addresses on Sending IPs, then assign here.`}
action={
<div className="flex items-center gap-3">
<Link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function OrganizationsPage() {
<PageFrame>
<PageHeading
title="Organizations"
description="Open any row for the full hub — members, domains, API keys, templates, emails, webhooks, support, and audit."
description="Open any row for the full hub: members, domains, API keys, templates, emails, webhooks, support, and audit."
meta={
<span className="rounded-full bg-bg-weak-50 px-2.5 py-1 font-medium text-[12px] text-text-sub-600 tabular-nums dark:bg-white/[0.06]">
{data?.total ?? 0} total
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function OverviewPage() {
<PageFrame>
<PageHeading
title="Overview"
description="Platform health and queues — open full hubs from any signal, not thin lists."
description="Platform health and queues. Open full hubs from any signal, not thin lists."
actions={
<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type SearchResponse = {
};

function warmupLabel(warmup: WarmupView | null): string {
if (!warmup) return "—";
if (!warmup) return "-";
if (warmup.status === "completed") return "Warmed";
if (warmup.status === "paused") return "Paused";
if (warmup.status === "pending") return "Scheduled";
Expand Down Expand Up @@ -400,7 +400,7 @@ export default function SendingIpsPage() {
}
/>
) : (
"—"
"-"
)}
</td>
<td className="px-4 py-3 text-right">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export function SupportContextPanel({
);

const displayName = user?.name || conversation.userName || "Unknown";
const displayEmail = user?.email || conversation.userEmail || "—";
const displayEmail = user?.email || conversation.userEmail || "-";
const displayImage = user?.image || conversation.userImage;
const role = user?.role || "user";
const isPlatformAdmin = role === PLATFORM_ADMIN_ROLE;
Expand Down Expand Up @@ -324,7 +324,7 @@ export function SupportContextPanel({
{orgLoading
? "…"
: balance === null
? "—"
? "-"
: formatNumber(balance)}
</p>
{lowCredits ? (
Expand Down Expand Up @@ -360,14 +360,14 @@ export function SupportContextPanel({
) : null}
</div>

{/* Act now — credits */}
{/* Act now: credits */}
<div className="space-y-2 border-stroke-soft-100 border-b px-4 py-3 dark:border-stroke-soft-100/40">
<p className="font-semibold text-[11px] text-text-sub-600 uppercase tracking-wide">
Resolve with credits
</p>
{!org ? (
<p className="text-[12px] text-text-sub-600">
No organization linked — open the user hub to pick context.
No organization linked. Open the user hub to pick context.
</p>
) : showTopupConfirm ? (
<InlineActionPanel
Expand Down
Loading
Loading