Open-Source Cold Email & Warmup Platform
Warmbly 是開源的冷郵件平台:溫暖您的收件箱、發送活動、並對每個回覆進行分流。可在我們的雲端運行或自行託管整個系統。
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Single-operator outbound publishing service for the WitUS ecosystem. Sibling to witus-inbox; routes signed-webhook publish requests to Ocoya.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Single-operator outbound publishing service for the WitUS ecosystem. Sibling to witus-inbox.檢視完整 README ↗Single-operator outbound publishing service for the WitUS ecosystem. Sibling to witus-inbox.
POST /api/ingest from any registered ecosystem product.scripts/import-radaar-csv.ts (the consultant calendar at gemini/witus/plans/social-media/consultant-response/she_clocked_in_radaar_import.csv).PublisherAdapter interface in lib/publishers/. Switching backends = add an adapter file, flip one env var./api/admin/tick; outbox dispatches to the active adapter.error at publish time./outbox for manual retry / reschedule / cancel / reconcile-now.DB columns are publisher-agnostic (publisher_backend, publisher_post_id, publisher_profile_id). Apps Script holds zero publisher credentials. Social-account changes need no code change — the next tick refreshes the cache from whichever adapter is active.
ADMIN_EMAIL)@sentry/nextjs SDK pointed at Better StackPoint uptime monitors at GET /api/health, not at /. The homepage can return a cached 200
while Postgres is down, so a green check there means nothing. /api/health is public,
unauthenticated, never cached (force-dynamic + Cache-Control: no-store) and runs one real
select 1 against Neon on every request:
| Condition | Status | Body |
|---|---|---|
| Database answers | 200 | {"ok":true,"checkedAt":"<ISO 8601>"} |
| Database unreachable, erroring, or slower than 4s | 503 | {"ok":false,"error":"database_unreachable"} |
The failure body is that fixed token and nothing else. Neon connection errors routinely embed the
connection string including the password, so the raw error never crosses the response boundary;
only the content-free error class name and SQLSTATE (via lib/db-safe.ts) go to the server log.
It checks the database and nothing else. It deliberately does not call Ocoya, SocialChamp, Mailgun or Mobile Text Alerts, and reports nothing about which publisher backend is configured or whether its token is valid: a vendor outage is not an outbox outage and must not turn the monitor red, provider error bodies echo bearer tokens, and this endpoint is open to the internet.
Not to be confused with GET /api/admin/health, which is bearer-authed and answers a different
question: "did the Apps Script reconciler tick recently, and what last broke?"
The reconciler itself is watched by a Better Stack heartbeat: /api/admin/tick pings
BETTERSTACK_HEARTBEAT_URL only at the end of a successful tick. A missed heartbeat IS the
signal — it means Apps Script stopped calling or the tick started failing — so failure paths
deliberately do not ping. Inert until the env var is set; the ping is awaited with a short timeout
and can never fail the tick itself.
Server, edge and browser errors report through the @sentry/nextjs SDK to a Better Stack source. The
SDK is guarded on the DSN: with SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN unset, init() never runs
and the whole layer is inert (__tests__/sentry-inert.test.ts guards that). Set the vars listed in
.env.example to turn it on; plans/user-tasks/16-error-monitoring-dsn.md has the operator steps.
Because outbox holds the ecosystem's publishing credentials, lib/sentry-scrub.ts runs a strict
beforeSend pass on every runtime: request bodies are dropped outright, cookies and credentialed
headers are deleted, captured local variables are dropped, and publisher API keys, ingest HMAC
secrets, social-platform OAuth tokens, JWTs, connection-string passwords and email addresses are
redacted out of messages, exception values, breadcrumbs, tags and extras. lib/sentry-scrub.test.ts
asserts the serialized event contains none of them.
app/global-error.tsx is the last-resort boundary for a crash in the root layout itself, which
error.tsx cannot catch because the layout is the thing that broke. It renders its own
<html>/<body> with inline styles and imports nothing but the Sentry SDK, so a broken component or
an unloaded stylesheet cannot take the error page down with it.
There is no Content-Security-Policy in this repo (no headers() in next.config.ts, no CSP in
proxy.ts, no <meta http-equiv>), so no connect-src has to name the ingest origin. If a CSP is
ever added it must list the DSN's origin, or the browser silently drops every client-side report and
the dashboard just looks quiet.
Traces go to Honeycomb over OTLP via @vercel/otel (otel.config.ts, registered from
instrumentation.ts before the Sentry configs load — whoever registers the global tracer
provider first wins, and Sentry is told to stand down via skipOpenTelemetrySetup in
sentry.server.config.ts). Service name is witus-outbox.
HONEYCOMB_INGEST_API_KEY_SECRET (fallback HONEYCOMB_API_KEY).
Same inert-until-provisioned pattern as the Sentry DSN — with neither var set, registration is
skipped entirely./api/health spans are dropped at the sampler — Better Stack probes it around the clock, and
those requests must not spend Honeycomb's free-tier event budget. Everything else is recorded
unsampled.@vercel/otel honors incoming W3C traceparent headers, so a publish request signed by a
sending ecosystem app continues here as the same distributed trace.Anonymous page-view analytics go to the ecosystem's shared PostHog project (US region), wired
exactly per witus-online's lib/analytics/INTEGRATE.md:
lib/analytics/posthog-provider.tsx and lib/analytics/capture.ts are byte-for-byte copies of
witus-online's files — don't edit them here; the init options are the ecosystem privacy posture.
lib/analytics/events.ts is this app's own taxonomy (ANALYTICS_APP = "outbox", the identity slug
from witus-online lib/identity/clients.ts).autocapture: false, session replay disabled, persistence: "memory" (no cookie,
no localStorage — so no consent banner), no identify(). Every event carries app: "outbox".
Because identity lives for one page session, treat "users" as "sessions"./ingest (rewrite in next.config.ts → https://us.i.posthog.com), so
ad blockers don't drop events. skipTrailingSlashRedirect: true is required for that. proxy.ts
uses explicit-path matchers, so it never touches /ingest. No app route may start with /ingest.NEXT_PUBLIC_POSTHOG_KEY (and NEXT_PUBLIC_POSTHOG_HOST, set
explicitly to https://us.i.posthog.com). Redeploy after setting — NEXT_PUBLIC_* is build-time.capture() from lib/analytics/capture.ts with a name from EVENTS.node scripts/check-posthog-conformance.mjs /path/to/witus-outbox.Playwright specs live in e2e/; the gate runs in .github/workflows/e2e.yml on
deployment_status — it tests the real Vercel deployment URL (preview → full suite,
production → @smoke only), so CI needs no secrets, database, or env. The suite runs desktop plus
a 360px mobile project, and every covered page must pass an axe check with zero serious or
critical WCAG A/AA violations — the gate is strict on purpose; fix the page, not the gate.
PLAYWRIGHT_BASE_URL=<url> npx playwright test (drives installed Chrome via
channel: "chrome"; Playwright's bundled chromium doesn't support macOS 13).VERCEL_AUTOMATION_BYPASS_SECRET Actions secret; public previews need
nothing.x-witus-origin-test: playwright-synthetic, which the OTel layer surfaces as the
witus.origin_test span attribute — Honeycomb queries (and logs/analytics) can include or
exclude test traffic. Absent header = attribute absent = real user.Optional. Set WITUS_OIDC_CLIENT_ID (witus-outbox) and WITUS_OIDC_CLIENT_SECRET (equal to
WITUS_OIDC_SECRET__OUTBOX on the witus.online IdP) and /auth/sign-in offers "Sign in with WitUS"
alongside the email magic link. Both are read at request time on the server, so setting them and
redeploying is enough; there is no separate client-side flag (the old NEXT_PUBLIC_WITUS_SSO is no
longer read). The client is registered in the witus repo's lib/identity/clients.ts (slug outbox)
with the redirect URI https://outbox.witus.online/api/auth/callback/witus.
ADMIN_EMAIL gate as the magic link
(lib/auth-gate.ts), and additionally need the IdP's email_verified claim to be true.email_verified check, which NextAuth runs before it links.id_token_signed_response_alg: "EdDSA" (openid-client defaults to RS256 and would fail at the
callback)./auth/sign-in?error=<code> with a notice
(lib/auth-error.ts) instead of NextAuth's bare error page.Two more behaviours ride on that config, and both stay completely dark without it:
<idp-origin>/api/ecosystem/session (derived from WITUS_OIDC_DISCOVERY_URL, never hardcoded a
second time) whether this browser already has a WitUS session. If it does, the button relabels to
"Continue as <name>". A blocked, failed or timed-out probe is invisible — no error, no
spinner, no layout shift — which is the common case on Safari and Firefox, where the IdP cookie is
a partitioned third-party cookie. The name is display copy only: clicking still runs the real OIDC
code flow, which is the only thing that establishes identity. A one-shot marker
(sessionStorage["witus.sso.attempted"], plus a ?sso=tried query param) is written before
the redirect so a stale IdP session cannot produce a sign-in loop./auth/sign-in; your WitUS session
and every other WitUS app stay signed in. With SSO configured, a second, secondary control,
Sign out of WitUS everywhere, also ends the shared session at accounts.witus.online; its help
text says other WitUS apps stay signed in until you sign out of them. That control destroys the
local NextAuth session first, then hands the browser to the IdP's end_session_endpoint, so an
unreachable or refusing IdP still leaves you signed out here. Without SSO configured only
Sign out shows.Design and reasoning: lib/silent-sso.ts. Tests: lib/silent-sso.test.ts.
Phase 1 in progress. Repo bootstrap. See plans/01-witus-outbox-bootstrap.md (local-only) for the full approved plan.
External steps that block Phase 1 smoke testing live in plans/user-tasks/.
Inbox is receive-only; outbox is send-only. Mixing the two in one app would couple unrelated lifecycles, secrets, and rate-limit budgets. Separation lets each service own one job — matching the WitUS ecosystem's "every platform has one job" rule.
See LICENSE when added.