DeepSeek Harness Remote
DeepSeek Harness Remote: DeepSeek Harness 실행 PC와 모바일·PC를 안전하게 연결해 어디서나 작업 이어받기
A serverless webmail client for JMAP — static files, your mail server, nothing in between.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Waxwing is a modern, minimalist webmail client that runs entirely in the browser and talks directly to a JMAP mail server such as Stalwart — over HTTPS, with live updates on an SSE stream. It ships as plain static files: Stalwart can host it itself (via its Applications feature), or you serve it from any web server or CDN. Installable as a Progressive Web App with real push notifications, offline reading, and an offline outbox.
The waxwing is named for the sealing-wax-red tips of its wing feathers — the bird that carries a wax seal. Silky plumage, no excess, letters delivered sealed: that's the brand.
Classic webmail (Roundcube & friends) needs its own server-side stack because IMAP was never meant for browsers. JMAP is: JSON over HTTPS, parsed and indexed by the mail server. That makes the extra webmail server obsolete — Waxwing is the client that follows through on that idea.
Stalwart can host Waxwing itself and keep it updated. One command, and it is done:
curl -u 'admin:PASSWORD' -X POST https://mail.example.com/jmap/ \
-H 'Content-Type: application/json' \
-d '{
"using": ["urn:ietf:params:jmap:core"],
"methodCalls": [["x:Application/set", {
"create": {
"waxwing": {
"enable": true,
"description": "Waxwing webmail",
"resourceUrl": "https://github.com/Heiko-W/waxwing/releases/latest/download/waxwing-stalwart.zip",
"urlPrefix": { "/webmail": true },
"autoUpdateInterval": 604800000
}
}
}, "c1"]]
}'
Restart Stalwart and open https://mail.example.com/webmail/. That is the whole installation:
no web server to run, no container, no database.
That is the Stalwart 1.0 spelling. Stalwart 0.16 calls the two fields enabled and
autoUpdateFrequency — docs/deployment.md lists everything
that differs.
autoUpdateInterval makes Stalwart re-fetch the release weekly, so new versions arrive on
their own. Say plainly what that means: it is a standing decision to run whatever this project
publishes next, in your users' browsers, against their mailboxes. If you would rather not,
point resourceUrl at a versioned asset and drop the field — then upgrading is deliberate, and
you can check what you are upgrading to:
sha256sum -c SHA256SUMS --ignore-missing # arrived intact
gh attestation verify waxwing-stalwart.zip --repo Heiko-W/waxwing \
--source-ref refs/tags/v0.26.1 # built here, from THAT TAG
--ignore-missing because SHA256SUMS lists all three artefacts and you downloaded one;
without it the command fails on the two you did not fetch. --source-ref because without it
the check also passes for a build from any branch of this repository — including the rehearsal
builds the release workflow makes on purpose. Name the tag you are installing.
The deployment guide has both forms, and
SECURITY.md §4 says what each check does and does not cover.
Not using Stalwart, or want it behind your own nginx? Two more paths — and the honest trade-off of the cross-origin one — are in the deployment guide.
config.json + theme.css
with no rebuildv0.26.1 — feature-complete, and deliberately not 1.0 yet.
Every planned work package is done and the release gate is signed off: 6 053 unit tests, 20 integration tests against a live Stalwart, and 259 end-to-end tests (two more skipped) across the seven Playwright suites the gate runs, on Stalwart 1.0; the CI runs the same suites against 0.16.27 as well. Performance and accessibility are measured rather than asserted — the numbers are in the implementation plan.
v0.26.1 is two small fixes from daily use. Triage stacks Undo toasts, which by design (ADR-021) never expire; from two of them on there is now a Dismiss all button that closes the pile without running any Undo. And the unread count on the installed app's icon has always followed the notifications switch in Settings → Notifications, which nothing there said — a sentence under the switch says so now, shown only where it is true (the browser has the Badging API and the permission is not blocked). Whether macOS actually paints the badge has not been checked on a real Mac.
v0.26.0 runs on Stalwart 1.0 and on 0.16.27, and the same suites are green on both. Stalwart
1.0.0 shipped on 26 October 2026 and its release notes name almost nothing a client sees. Run
against the same fixture, eight things broke or changed, two of them badly: the release bundle
could not be installed at all (1.0 refuses a bundle without stalwart-app.json, and reports success
while it does), and signing in with OAuth stopped working (1.0 answers client_id=waxwing with
"Unknown client id"). The Files screen, the calendar month and shared address books and calendars
broke too, and the people search in the share dialogs finds nobody until an administrator allows
directory queries. All of it is fixed and tested; ADR-048
has the table. If you run Stalwart, upgrade Waxwing before you migrate it to 1.0 — a migrated
server that still holds an older bundle has no webmail until the next fetch. The CI now runs the
whole gate against both servers. The release also takes DOMPurify to 3.4.17, which closes a
critical and a high advisory in the library that cleans every HTML mail (both concern options
Waxwing does not set — SAFE_FOR_TEMPLATES and IN_PLACE — which is why it was not urgent, not why it
was skipped), and 13 more dependency alerts. Five development-only alerts on Vite stay open on
purpose: the fix is in Vite 8.1.6, whose bundler splits the same source into different chunks and
takes the initial JavaScript from 295.46 to 310.19 kB, over the 300 kB budget. They concern Vite's
dev server, not what ships.
New with 1.0, in Settings → Account & security: the apps signed in to your account — what each may reach, when it was last used, from where — and a Sign out for each. Signing out of Waxwing now also revokes the token on a 1.0 server, which Stalwart 0.16 could not do (checked against a real server: the sign-in's grant is gone from the account afterwards). Searching for a word inside a PDF attachment works on 1.0 without any change here — the server indexes attachment contents now (its notes list Office, OpenDocument, EPUB and RTF too; the PDF is what was measured), and Waxwing's search asks it.
v0.25.1 is compatible with Stalwart up to v0.16.25. The end-to-end suites now run against
Stalwart v0.16.25 (they ran against v0.16.18). The first run found one break that no Stalwart
changelog mentions: from v0.16.20 the account language is spelled de-DE, not de_DE, and the
server refuses the old spelling — so "Language of server messages" could not be saved on any newer
server. Waxwing now reads either spelling and writes the one the server uses, so v0.16.18 through
v0.16.25 all work: the whole suite is green on v0.25.1 / v0.16.25, and the language setting is
also checked against v0.16.18 and v0.16.19 (the old spelling) and probed on v0.16.20–v0.16.22. The
release also takes DOMPurify to 3.4.16 (both of its advisories concern the IN_PLACE mode, which
Waxwing does not use) and closes the nine other open dependency alerts, all of them build- or
test-time.
v0.25.0 draws every account's calendars in one grid. A shared or group calendar used to be reachable one account at a time — an entry in the rail, and the whole screen switched to it. That is the wrong shape for a calendar: the question a shared calendar exists to answer is "when are we all free", and a screen showing one account at a time cannot answer it. Now every calendar-serving account is in the same month, week and agenda, each calendar with its own section in the rail, its own tick and its own colour (ADR-047).
Two consequences worth naming. The tick is this device's decision now, not the account's — it
used to write the server's isVisible, which is impossible on a calendar shared with you read-only,
so half the rows had a tick box that could not be ticked. And events wear their calendar's
colour, including calendars the server left without one: Stalwart's default calendar has no
colour, so "leave colourless calendars neutral" would have drawn both accounts in the same chip —
the one thing the merged grid exists to prevent.
v0.24.1 closed every open dependency advisory, including one that had been held open. Seven
alerts, six of them high, all in build- and test-time packages — the shipped bundle is byte-for-byte
the same size. The esbuild one had been documented as a deliberate exception since 2026-08-18
because the fix steps outside tsup's declared range; tsup has not moved, but vite — the
toolchain that builds the bundle — accepts the newer major and develops against it, and
build:libs on it emits byte-identical output. Measured, not assumed. Nothing was dismissed.
v0.24.0 opened what other people have shared with you — and fixed the reason it could not work. A shared calendar or address book has appeared in the session since S-1 and been unreachable in the client ever since. It now has a place: the contacts rail groups books by account, the calendar rail gains an account entry, and a delegated book opens in ITS account rather than in the same-id book in your own — JMAP ids are per-account and short, so that collision is real (ADR-018). The rails came from a contributed pull request (#68).
They drew nothing, and the reasons were underneath them. The client reads its contacts and calendar
out of the replica, only a sync engine writes there, and only a MAIL account got an engine — so an
account that shares an address book and no mailbox had no rows to draw, indefinitely, while
AddressBook/get was returning the book to the same session. Chasing that turned up an older defect
that is not about sharing at all: Identity/get is refused on every delegated account, and that
refusal was killing the whole sync pass. Every shared mailbox has silently not synced its
contacts, calendar and files since the engine fleet existed — its mail worked, which is why nobody
noticed (ADR-046).
v0.23.0 was the September code review, worked to the end — and the two Musts it left open.
112 findings from a review in eight dimensions, each one adversarially re-checked before it was
believed, each fix pinned by a regression test that goes red when the fix is removed. The full
list is in docs/reviews/2026-09-01-code-review.md.
Five of its own recommendations were disproved while implementing them and solved another way;
the reasoning is at each finding.
Two of the findings were Musts the plan had parked, and both are now built:
Email/query, and the selection is
a snapshot rather than a subscription — so "300 selected" means the same thing when you press
Delete as it did when you selected
(ADR-042). Undo carries the full set.The defects worth naming are the ones nobody had filed: a draft lost its Bcc recipients on
open-and-close, blank lines were dropped from sent mail in both composer modes, a signature
change was silently discarded in plain-text mode, a delivered message could be reported as "send
interrupted", and two buttons looked usable while refusing every click — aria-disabled with no
visual half, one of them since M1.4. That last one was found by looking at the built app on a
phone and a tablet with the network actually off; 5 900 green tests could not see it.
Importing 500 contacts takes 450 ms instead of 15.4 s.
One more finding came out of the release gate itself, and it is the kind worth publishing:
the focus sweep had never walked a whole tab order. It started each walk with
document.body.focus(), which in Chromium moves nothing — so it began wherever the last click
had left focus and covered 11 stops of 24 on the list, 5 of 28 on the reading pane, 5 of 37 in
the composer. The WCAG 2.4.7 and 1.4.11 verdicts this project has been reporting were measured
over a fraction of each screen. The walk is anchored now, asserts where it started, and is green
across every stop (ADR-045).
v0.22.0 was a security and implementation review, worked to the end. Forty findings from a
review in seven dimensions — each one adversarially re-checked before it was believed, and each fix
pinned by a regression test that goes red when the fix is removed. The full list, with what was
found and what was decided, is in
docs/reviews/2026-08-29-security-code-review.md.
The two that mattered most were both about a promise the code did not keep:
" inside one opened a string state the splitter
had and the browser did not — after which no ; was a declaration boundary and the whole
attribute fused into one allowlisted color, carrying a full-screen position:fixed overlay
with it. In the composer's quote path that lands in the app's own DOM.Two findings are deliberately only half-fixed, and say so where it matters: ADR-037 records that the account switcher ships without the per-account store ADR-004 designed — the credential path is closed, the menu now says "Sign in as …" rather than pretending the switch is free — and the cache reaper prunes calendar occurrences but not contact cards, because a card is the address book rather than a cached view of it.
v0.21.0 was twelve more languages — cs es fr it ja nl pl pt ru tr uk zh, fourteen in all.
A Stalwart operator asked for Russian, and asked in the same sentence whether they should fork the
project to get it (#50). docs/translating.md had
ruled machine translation out on grounds that are correct — a reader who confuses Discard with
Archive loses a message — and drawn the wrong conclusion from them. The pipeline that sentence
pointed at had produced no language in the whole time it existed, so the choice was never between a
machine translation and a reviewed one. It was between a machine translation and English.
ADR-036 is the argument;
translating.md, Contributing and the answer on
#50 all name which twelve bundles nobody has read.
What the gate takes off the table is every failure a reviewer cannot see anyway: key parity, the
plural forms each language actually selects (Russian, Ukrainian, Polish and Czech have four where
English has two, and a missing _few falls back to English mid-sentence for the counts 2–4), an
invented {{placeholder}}, a dropped {{product}}, a hardcoded brand name, a file that is more
than half English verbatim. What is left is wording — and wording is a one-line pull request.
Two real defects surfaced only because the bundle stopped being two Latin languages, both reported independently by two of the translators:
\b, which is defined
against ASCII \w, so a keyword beginning with в, 附 or 添 cannot sit on such a boundary at
any position — including the start of the message. "You mentioned an attachment but attached
none" was silently dead in Russian, Ukrainian, Japanese and Chinese, for 19 of 19 keywords. Han
and kana get no boundary at all now, because a script without word gaps has nothing for one to be.toLowerCase() folds Turkish İ to i plus a combining dot, so a mail opening "İlişikte…"
matched nothing. The fold takes a language now.Both mutation-proven: three of the new tests go red against the old implementation.
The cost is stated rather than hidden: the service-worker precache grows from 478 KB to 769 KB gzipped, of which 334 KB is the fourteen locale chunks. No budget is broken — initial JS is 283.94 KB gz of 300, and locale bundles have always been lazy.
v0.20.0 is the §13 finding list, worked to the end — ten Bxx findings fixed, three closed as
accepted trade-offs, one left open on the owner's decision.
Three of them were not tidying:
upToId in deciding which changes to report and
numbers the additions against the whole result set, so an "unread first" window of 50 over 120
messages answers removed: 50, added: 50 with every addition at index 70–119. The client dropped
all fifty and wrote the window back empty, with a carried-over total and a live query state —
and only a voided window is ever re-queried. The folder would have shown nothing, online,
indefinitely. An addition that cannot be placed now means "this delta cannot be applied", which is
what cannotCalculateChanges already means.e still dispatched a move into the mailbox
that had just been deleted. One shared subscription now — which also removed an accident another
component was quietly living on, and is the more interesting half of that story.<iframe> matches neither :focus, :focus-visible nor
:focus-within and fires no focus event, while the framed document reports hasFocus().Four security questions, answered by experiment rather than by argument (B25). The app's CSP
does bind inside the mail frame — the intersection of the two policies applies, which
csp.shipped.test.ts has reasoned from for two milestones without anything ever measuring it. Five
namespace-confusion payloads pasted through the real paste path leave nothing behind. And a JMAP
navigation cannot reach the service worker under a /mail/ mount, because scope decides that, not
the denylist.
WebKit is in the gate now (B11). It used to be four smoke tests run by hand; it runs the whole read suite — the sanitizer, the reading pane, the split pane, the message list — on the one engine whose disagreements have actually cost this project defects. One test is skipped there with the reason at the skip (ADR-029).
Three findings are closed as accepted rather than fixed, each with the condition under which it would be re-opened: an inexact window retraction whose exact form needs a versioned undo payload (B14), a void gate that is deliberately not filter-aware because over-voiding costs a round trip and under-voiding costs a wrong list (B15), and a count patch that is not re-applied for an intent the server may already have processed, because a double-count never self-corrects (B18).
One stays open: content in a push notification while the app is closed (B28) would move the
access token, the SecretStore and the OAuth refresh into the service worker. Nothing of that is
there today, and that is what NFR-SEC-02 promises. Post-V1, on the owner's decision.
v0.19.0 is Waxwing read against Apple's Human Interface Guidelines — 39 findings for the desktop, the tablet and the phone, all of them worked off (the survey).
Full conformance was never the goal, and the audit says so on its first page: this is a web app that also runs on Windows, Linux and Android, and part of the HIG presupposes a system menu bar, window traffic lights and real vibrancy materials. What transfers is the other part — and it turned out to be the larger one. Every finding carries a verdict of adopt, adapt or deliberately differ; the third category came out empty, which is itself a result.
Five causes produced twelve of the findings:
color-scheme was declared nowhere. The tokens could paint the app dark; the parts the
browser draws stayed light — scrollbars, the open list of every native <select> (a system
popover on an iPad), date fields, the selection highlight.animation-iteration-count: 1 for everything meant the spinner turned once in a hundredth of a
millisecond and then stood still: the app told its reader "stalled" with the very mark that means
"working". WCAG 2.3.3 asks for reduced motion, not for none.visualViewport appeared zero times. Send, Discard and every dialog footer sat under the
on-screen keyboard — the primary action of this app's central task, invisible for as long as
someone was typing.viewport-fit=cover and display: standalone.
The phone header — which carries the entire mail chrome on that viewport — padded 8 px against
the Dynamic Island.Plus context menus on four surfaces, ⌘Z for undo, grouped menus with separators, a folder rail that can be put away and stays away, a split whose width is a share of the room rather than a per-tier constant, an action bar docked in the thumb zone on a phone, and keyboard semantics for address fields.
Two findings are decided rather than fixed, each with an ADR: the PWA launch screen cannot
follow the system appearance because the manifest holds one background_color and no media-query
mechanism (ADR-033), and
upload progress is not available behind the fetch seam
(ADR-034).
What the checks are for. Every one of those defects is invisible on a developer's machine, so
the round leaves five new static guards behind — color-scheme per theme block, env(safe-area-*)
only in tokens.css (the inset is physical, so a hand-written rule pads the wrong edge under
RTL), an explicit variant on every <Button>, the first test layout.ts has ever had, and the
contrast matrix over four palettes instead of two.
And what only looking found. A visual sweep at 390 / 834 / 1280 px against the live fixture caught a defect six passing assertions could not: the header reported "Updated in 9 seconds" — a time in the future, for a minute after every sync. The clock was read when the shell mounted and kept in state; every test asked about "3 minutes ago", and the sign only flips in the first moment.
v0.18.0 is the folder rail, read from two screenshots of the running deployment. With three accounts in it the rail was one long column of near-identical folder names, and the top-level navigation spent 96 px repeating what its icons already said:
z-index, and a stacking context is what gets painted over
an overlay scrollbar. It cannot happen where the bar has its own gutter, which is why no suite
here could have seen it: they all run Chromium on Linux. The app now measures the platform's real
scrollbar at boot and reserves a lane only where one is needed.The flake that came with it was two test defects, and neither was in the app. One assertion counted a list before it had arrived and then waited five seconds for a correct icon to disappear; another aimed a click by hovering, in a rail that moves when a share notice arrives. Sixteen consecutive runs of the suite passed afterwards, and CI recorded no retries for the first time since v0.17.0. A third instance of the same class turned up in the test written to prove the first fix — recorded, with the rule it produced: if a value crosses a store, a database or the network, wait for it.
v0.17.0 is what the traces found. Six end-to-end suites had been failing intermittently for months and it was being read as flakiness. It was not. Recording what actually went over the wire — rather than reasoning about the code — turned the same symptom into five separate defects, four of them in the app:
The harness was hiding all of it. Stalwart rate-limits per account, and the suites drive one account through a hundred sign-ins in five minutes where a real client's entire sign-in is fourteen requests — so the app kept meeting a budget the previous hundred tests had spent (ADR-031). Opening a window is also one request rather than two now, using the back-references JMAP has for exactly that (ADR-032).
v0.15.0 closed the JMAP gap. A survey of what Stalwart offers against what this client used
produced 67 findings; 58 are implemented here. Calendars can be created and shared, series and
reminders edited, invitations sent and answered, .ics files imported; folders reorder, hide and
take a role; mail search leaves the trash out and understands OR, NOT and size; push
notifications carry sender and subject; files move, multi-select and search; calendars and files
read from the replica when there is no network; and app passwords and the account password can be
managed without leaving the client.
The survey was run against two live servers rather than against documentation, and that is what
it was worth. The pinned test fixture had drifted a version behind production — several
findings were untestable until it moved. Four plans written from drafts were contradicted by
the server, one of which (inCalendars for inCalendar) would have emptied the month view the
first time anyone hid a calendar. The end-to-end suites caught four bugs this release itself
introduced, with the unit tests green for all four. And the visual pass over 72 surfaces at
three widths stopped seven more, including a menu behind its dialog that made renaming a calendar
unreachable on a phone. The findings, the measurements and what was deliberately not built are
in the survey.
v0.14.0 was the second UI review, fixed. Sixty findings from a follow-up pass over the running client at three viewport tiers, in both themes and across all six accent palettes — including two settings that turned out to apply only half-way (the dark palette had four surface tokens on one value, and five of six accents left the selection tint blue), and a regression the previous round's own work had introduced. The list, its evidence and the three findings withdrawn on reading the code are in the review.
v0.13.0 closed the gap to Bulwark, the other serverless JMAP client, measured feature
by feature in the comparison. Sieve filter rules, a
calendar, file storage with sharing, multiple accounts, scheduled send, saved searches,
templates, snooze, winmail.dat unpacking and read receipts all arrived in that release. Every
JMAP shape in it was measured against a live server rather than transcribed from a draft, which
is how four of them turned out to differ from what the specification says.
One gap is left open on purpose: PGP and S/MIME signatures are recognised but not verified. §5.1 of the comparison says which half is buildable and which is blocked, and why a tick this client has not earned would be worse than no tick at all.
What 1.0 is waiting on is use. A mail client earns that number by being lived in for a while, against more than one server and more than one mailbox. That has not happened yet.
Known gaps, stated plainly:
docs/accessibility.md.SECURITY.md says why in detail.Running it
config.json reference — every setting, with its range and reasoningBuilding on it
Contributions are welcome, and the project is unusually explicit about what it expects — see
CONTRIBUTING.md. The short version: pnpm gate has to be green, and a
test has to fail when your fix is removed. That second one is the house rule; the guide
illustrates it with three real cases where a green test was measuring nothing.
There is no curated starter list — pick something that bothers you. Bug reports are genuinely
useful on their own, especially with a .eml attached for anything about how a message renders,
which is the only way to reproduce those exactly.
Waxwing speaks fourteen languages. Two of them — English and German — were written by a person.
The other twelve were machine-generated, checked mechanically (every key present, the plural
forms each language actually selects, no invented {{placeholder}}, no hardcoded brand name) and
read by nobody who speaks them. ADR-036
is the argument for shipping them anyway; the short version is that the alternative on offer was
English for everyone.
So if you read one of the twelve, the most valuable thing you can do here is tell us where it
sounds wrong. Fixing a string is one line in one JSON file — apps/web/src/i18n/locales/<lang>/common.json
— and node scripts/check-locales.mjs <lang> tells you in a second whether the file is still
valid. translating.md has the rest. An issue that just says "this button
says the wrong thing" is welcome too.
The app is AGPL-3.0-only: run it, modify it, host it for others — and if you modify it and let people use it over a network, they get your changes too.
Two packages are MIT so that clients which are not themselves AGPL can use them:
@waxwing/jmap (a typed JMAP client, no runtime dependencies) and
@waxwing/jscontact (JSContact ⇆ vCard). Neither is on npm yet. The cut is
enforced by the dependency direction: neither MIT package imports AGPL code.
Everything else — apps/web, packages/mail-html, e2e, scripts, docs — is AGPL-3.0-only.
This explanation used to sit at the top of LICENSE, which cost the project its licence
badge: GitHub reads that file to identify the licence, ten lines of preamble made it
NOASSERTION, and a license:agpl-3.0 search did not return this repository at all. LICENSE is
now the verbatim AGPL-3.0 text — diff against gnu.org's copy is empty — and the scope note lives
here, where a reader was going to look anyway.
Prerequisites: Node.js 24 — the version in .nvmrc, and not just a
recommendation — and pnpm ≥ 10 (corepack enable picks up the version pinned in
package.json). nvm use is enough.
This line used to say "≥ 22", and so did engines — which is how a newcomer ended up on Node 26:
install succeeded, the dev server ran, and then pnpm verify failed 54 tests with nothing in
eighteen thousand lines of output mentioning the Node version. Node ≥ 25 defines a global
localStorage that shadows jsdom's. Both ends are closed now: engines.node is ">=24 <25" and
engineStrict: true in pnpm-workspace.yaml makes pnpm install refuse the wrong major in a
second, and pnpm verify refuses to start on it as well.
pnpm install
Common scripts, run from the repo root:
| Command | What it does |
|---|---|
pnpm typecheck | TypeScript strict type-check across all workspace packages |
pnpm lint | Biome lint + format + import-sort check |
pnpm lint:fix | Biome auto-fix (lint + format + import sort) |
pnpm format | Biome format-write |
pnpm test | Unit/component tests (Vitest) |
pnpm build | Build all packages |
pnpm size | Build apps/web and check it against the size-limit budget (≤ 300 KB gz initial JS) |
pnpm verify | Run before committing — the fast gate, in this order: check:node → build:libs → typecheck → lint → test → size → check:dist → check:site → check:actions → check:nul (no Docker/browser) |
pnpm verify:e2e | The E2E gate (needs Docker): install chromium, bring the Stalwart fixture up + smoke, run Playwright, always tear down |
pnpm verify:all | pnpm verify then pnpm verify:e2e |
pnpm gate | The local pipeline — preflight (pins the Node major) → verify → the @waxwing/jmap integration suites against a live fixture → the E2E suites, with a per-stage summary |
pnpm gate:fast | The hermetic half of the pipeline; what .githooks/pre-push runs |
pnpm e2e:server | Start a local Stalwart JMAP server with test accounts (Docker) |
pnpm e2e:server:down | Stop it and wipe its ephemeral data |
pnpm demo | Dev-only raw end-to-end demo: Stalwart fixture + seeded mail + a throwaway login/read UI at http://localhost:5173 (Docker) |
pnpm demo --lan | Same, served on your LAN IP so another machine can open it (Basic sign-in only — see below) |
Before committing, run pnpm verify (and pnpm verify:e2e when you have Docker). These
scripts are the pre-merge gate: typecheck, Biome, tests, build, the size-limit budget, the
Stalwart fixture smoke and the Playwright suites. CI runs the very
same scripts — it was written that way on purpose (ADR-003),
so the local and hosted gates cannot drift apart.
pnpm gate)pnpm gate sequences those scripts and adds the three things a gate you run by hand cannot give you:
A Node preflight. The manifests and pnpm install reject the wrong major now, but a checkout
installed on 24 can still be RUN on another one, and nothing re-installs in between — on Node ≥ 25
a global localStorage shadows jsdom's and ~22 tests fail for reasons unrelated to the code. The
pipeline refuses to run rather than hand you results you would have to distrust.
The @waxwing/jmap integration suites, actually run (defect B22). They describe.skipIf
themselves away when the fixture is unreachable, so a skip was indistinguishable from a pass; the
stage brings a fixture up and then asserts nothing was skipped.
Automation. Enable the versioned hook once per clone:
git config core.hooksPath .githooks # pre-push runs `pnpm gate:fast`
Push anyway with git push --no-verify when you mean to.
.github/workflows/ci.yml runs the same stages by calling the same
scripts. Running it locally with act
was evaluated and rejected: act mounts the host Docker socket instead of nesting a daemon, so the
fixture's compose bind mounts resolve to non-existent host paths and are silently replaced with empty
directories — Stalwart then boots with no config and no diagnostic. Details in ADR-019.
Need a real mail server to develop against? pnpm e2e:server brings up a pinned, local,
plain-HTTP Stalwart instance with ready-made test accounts in one
command. See e2e/stalwart/README.md for accounts, ports, and
the dev-only TLS choice. Requires Docker.
pnpm demo)pnpm demo (work package SP.4) is a dev-only, throwaway UI that talks straight to the
JMAP fixture — login (OAuth + Basic), a mailbox list with counts, a paged message list, a
raw message view (text + naive HTML in a sandboxed iframe) and an Email/parse button for a
message/rfc822 attachment. It is not in the production bundle (it is gated on
import.meta.env.DEV && VITE_WAXWING_DEMO === '1', so every vite build dead-code-eliminates
it). One command brings up the Stalwart fixture, advertises the browser's origin, seeds
alice's inbox with demo mail, and starts a same-origin Vite proxy + dev server; Ctrl-C tears
everything back down.
pnpm demo # open http://localhost:5173 — Basic AND OAuth both work (secure context)
pnpm demo --lan # open http://<your-lan-ip>:5173 from another machine on your LAN
Sign in with the fixture account the banner prints ([email protected] /
waxwing-e2e-Pw1!; the form is pre-filled). LAN caveat: a plain-http LAN IP is an
insecure context, so crypto.subtle is unavailable — OAuth and "stay signed in" are
disabled there and only Basic sign-in works (the demo says so and disables OAuth). Serve
the demo over HTTPS at the LAN origin (or use localhost) if you need OAuth.
The matching Playwright check is pnpm e2e:demo (run it in a second terminal while
pnpm demo is up; it skips cleanly if the demo isn't running).
This is a pnpm workspace:
apps/web — the Waxwing SPA (AGPL-3.0)packages/jmap — @waxwing/jmap, typed JMAP client (MIT)packages/jscontact — @waxwing/jscontact, JSContact ↔ vCard 4 conversion (MIT)packages/mail-html — @waxwing/mail-html, HTML-mail sanitizer + sandboxed renderer (AGPL-3.0)e2e — Playwright suites + the Stalwart Docker fixturedocs — specification, tech stack, implementation plan, and ADRs