Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
293 commits
Select commit Hold shift + click to select a range
7bb9e79
ci: installer-validation workflow on self-hosted Windows/Linux runners
Jun 13, 2026
f5bf586
ci: use GitHub-hosted runners (public repo)
Jun 13, 2026
d422d5a
ci(installers): pin WiX extensions to the v6 toolset version (was res…
Jun 13, 2026
683e08c
ci(installers): use WixToolset.BootstrapperApplications.wixext (v6 dl…
Jun 13, 2026
d4b3c17
ci(installers): WiX v6 bindpath uses space syntax (-bindpath name=pat…
Jun 13, 2026
1d41331
ci(installers): give WixStandardBootstrapperApplication an empty Lice…
Jun 13, 2026
a512411
ci(installers): copy InstallSqlExpress.ps1 to launcher output so the …
Jun 13, 2026
976b5a8
fix(installer): gpg --dearmor needs --batch in non-interactive shells…
Jun 13, 2026
98477d7
fix(installer): install MS apt key into /etc/apt/trusted.gpg.d (list …
Jun 13, 2026
005128a
ci(installers): make the opt-in full-install smoke robust + self-diag…
Jun 13, 2026
8b03861
ci(installers): run linux full-install on ubuntu-22.04 (mssql-server …
Jun 13, 2026
a04ddb0
installer: use SQL Server 2025 Express (Linux apt/dnf 2025 repo, supp…
Jun 13, 2026
4ed7041
feat(providers): Amazon SES, Postmark, Resend, SparkPost, SMTP2GO + S…
Jun 13, 2026
2ae88b7
ci(installers): dump MSI per-package log + Windows event log on full-…
Jun 13, 2026
34e7d8c
fix(service): UseWindowsService + UseSystemd so the host reports read…
Jun 13, 2026
43c595c
ci(installers): add dormant SignPath code-signing step for the Window…
Jun 13, 2026
93e4912
ci: tagged-release pipeline with Azure Artifact Signing + Linux tarball
Jun 13, 2026
b6b7cbf
ci(installers): build + upload the Linux tarball as a workflow artifact
Jun 13, 2026
5147106
feat(docker): container image + compose stack; deployment-friendly CI…
Jun 14, 2026
53e532a
fix(config): empty allow-list means allow-all everywhere; add CIDR te…
Jun 14, 2026
f70dc91
ci+docs: GHCR multi-arch image publish on release; README Docker quic…
Jun 14, 2026
7beaed1
docs+ci: reconcile install docs with actual release assets
Jun 14, 2026
7fb87b7
docs: accuracy sweep of README against the codebase
Jun 14, 2026
6bea311
docs: reconcile auto-purge/retention section with the implementation
Jun 14, 2026
04ec998
docs(spec): reconcile SPEC.md with the implementation (5-section audit)
Jun 14, 2026
dac5c4d
feat(security): instant API-key revocation + SMTP AUTH brute-force lo…
Jun 14, 2026
3d0b362
docs: add SECURITY.md + GHCR make-public step in the release runbook
Jun 14, 2026
d8d556f
fix(api): scope GET /api/v1/messages/{id} status lookup to the callin…
Jun 14, 2026
456b08c
test(providers): unit tests for the 5 newer providers
Jun 14, 2026
5b19c7f
test: attachment ingestion, malformed-input, and broader injection-sa…
Jun 14, 2026
cc5ee20
test: silence nullable warning on MimePart.FileName dictionary key
Jun 14, 2026
2f3d00a
test: null-forgive MimePart.Content deref (0 warnings)
Jun 14, 2026
7cf3b0e
feat(pages): GitHub Pages site rendering the README (Slate dark theme)
Jun 15, 2026
737a494
fix(install.sh): install gnupg + apt prerequisites before importing t…
Jun 15, 2026
70dbd54
ci(pages): deploy from the feature branch too (temporary preview befo…
Jun 20, 2026
4ff7995
feat(pages): concise curated landing page instead of dumping the README
Jun 20, 2026
9775a7e
feat(pages): trim landing page to tagline + features + README/Release…
Jun 20, 2026
b75ae78
feat(pages): replace Jekyll/Slate with a clean dark Tailwind landing …
Jun 20, 2026
172196d
feat(install.sh): on arm64, fall back to Azure SQL Edge container for…
Jun 20, 2026
3685c4d
fix(install.sh): resolve relative --prebuilt against the script dir too
Jun 20, 2026
344b732
feat(install.sh): auto-select prebuilt binaries by CPU arch
Jun 20, 2026
eee6b82
ci(release): ship one universal Linux tarball (x64 + arm64)
Jun 20, 2026
47f7452
fix(install.sh): install libicu (+ ca-certs) for the self-contained b…
Jun 21, 2026
ebde8a7
fix(install.sh): resolve the version-numbered ICU package on Debian/U…
Jun 21, 2026
e053bc6
feat: HTTPS-only dashboard (self-signed by default) + move ingestion …
Jun 21, 2026
28ef8fc
fix(install.sh): stop the running service before copying binaries (up…
Jun 21, 2026
87f789b
docs(ui): clearer SMTP Authentication help text
Jun 21, 2026
eb53577
docs(ui): de-jargon Settings help text
Jun 21, 2026
b0957b8
fix(ui): dev proxy targets https (dashboard is HTTPS-only now)
Jun 21, 2026
17f623e
feat(ui): first-run setup wizard + catch-all relay model + tabbed set…
Jun 21, 2026
417114c
feat(security): audit fixes — CSRF, secure cookie/HSTS, at-rest key, …
Jun 21, 2026
afd6add
docs: document the reset-admin-password CLI + first-run wizard in the…
Jun 21, 2026
5839407
fix(data): actually commit migration 0003 (was untracked due to path …
Jun 21, 2026
b8aab6a
feat(ui): open message detail in a modal instead of a right-side pane
Jun 21, 2026
ff2724d
feat(ui): modal detail on Failed + Relays; show full recipients in th…
Jun 21, 2026
68df4db
feat(ui): reorganize the message detail modal into a summary + groupe…
Jun 21, 2026
dcf0c6a
feat(ui): consistent message-detail modal layout on Local Inbox + Failed
Jun 21, 2026
a0f048a
feat(ui): redesign the message-log filter bar
Jun 21, 2026
005690a
feat: log provider tests as real Delivered/Failed, identified by orig…
Jun 21, 2026
a41b03b
feat(ui): per-row actions menu + modal add on Relays & Routing
Jun 21, 2026
efe091c
feat(ui): hover highlight on actions-menu items
Jun 21, 2026
3729e5d
feat(ui): more row actions + fix menu hover + clearer routing order
Jun 21, 2026
32bda19
feat(ui): drop the redundant "Copy spool ID" button from message detail
Jun 21, 2026
3a6cc6e
feat: label dashboard test sends as "Dashboard test" ingest source
Jun 21, 2026
8831060
feat(ui): rename Failed→Retry Queue, group/tighten sidebar, SMTP Auth…
Jun 21, 2026
62f66b0
feat(ui): rename System→About; API Keys uses actions menu + modals
Jun 21, 2026
07dba1d
feat(tls): STARTTLS cert via generate-or-upload instead of a file path
Jun 21, 2026
d76308e
feat(ui): Local Inbox retention note + route error boundary
Jun 21, 2026
3b98434
feat(ui): deep-link from a Local Inbox message to its Message Log entry
Jun 21, 2026
c0ad4d1
feat(ui): dedicated Access Control page for SMTP/API allow-lists
Jun 21, 2026
27f74e1
feat(ui): combine ACL lists into one card with a target selector
Jun 21, 2026
8190b62
refactor(ui): redesign Access Control card
Jun 21, 2026
0688ef4
feat: closed-by-default access control for SMTP and API
Jun 21, 2026
2c9fce2
refactor(ui): subtabs within Settings pages instead of stacked cards
Jun 21, 2026
ae40a12
refactor(ui): cleaner Settings number fields + dirty-aware save
Jun 21, 2026
8591202
docs(ui): clarify the STARTTLS cert is SMTP-only
Jun 21, 2026
c781b3d
feat: HTTPS ingestion API + shared TLS certificate
Jun 21, 2026
32eebf4
fix(ui): constrain list pages so the add button aligns with content
Jun 21, 2026
f9c6c77
feat(ui): per-column filters in the Message Log header
Jun 21, 2026
5b7603c
refactor(ui): Message Log filter icon, per-column clear, fit columns
Jun 21, 2026
f652b42
feat(ui): show retry-queue auto-deletion window from settings
Jun 21, 2026
1d946d3
fix(ui): drop broken "View in Message Log" link from Local Inbox
Jun 21, 2026
1f224d0
feat(ui): show delivery-log details inline in the Local Inbox
Jun 21, 2026
c40270a
fix: clearer SMTP rejection when source IP is not allowed
Jun 22, 2026
674536a
feat: auto-provision a self-signed shared TLS cert on startup
Jun 22, 2026
4c362e1
feat(ui): show + download attachments in the Local Inbox popup
Jun 22, 2026
60a7370
feat: capture X-Mailer + attachment count and show in Message Log detail
Jun 22, 2026
d11ee5f
feat: add Maileroo provider (Email API v2)
Jun 22, 2026
64dad2a
perf(db): index relay_log spool_id and api_key_id lookups
Jun 22, 2026
a9b9b78
fix: guard null MimePart.Content when decoding attachments
Jun 22, 2026
8620e5f
feat(ui): Message Log refresh button + auto-refresh control
Jun 22, 2026
32e8c0b
feat(ui): refresh button + auto-refresh control on Local Inbox
Jun 22, 2026
c1e307d
fix(security): validate Mailgun Domain before path interpolation
Jun 22, 2026
1fefb12
feat: Reports page (date-range inbound + relay activity)
Jun 22, 2026
0a35439
feat: System Logs (audit trail) — phase 1
Jun 22, 2026
86ecc59
feat: audit config changes, access denials, relay failures
Jun 22, 2026
05720a5
feat: audit SMTP auth failures, provider tests, lifecycle
Jun 22, 2026
aa1fdd7
feat: audit-log retention (90d general, 7d security events)
Jun 22, 2026
700e80b
refactor: single database-size limit instead of trigger+target
Jun 22, 2026
b6e67b5
feat(ui): proper pagination for Message Log + Local Inbox
Jun 22, 2026
a25c3a0
test: SqlAuditLog write/query/purge integration tests
Jun 22, 2026
023626c
security: wipe the AdminPassword seed from appsettings after first run
Jun 25, 2026
98367d2
change: default Require HTTPS (dashboard) to on
Jun 25, 2026
e4e7420
change(ui): choose the catch-all relay on the Routing page
Jun 25, 2026
9b3623e
change(ui): drop the Role/catch-all column from the Relays table
Jun 25, 2026
d45bdfa
ui: confirm before clearing the Local Inbox
Jun 25, 2026
50fe6ac
ui(reports): add Today quick filter, drop redundant Last 30 days
Jun 25, 2026
b48f100
ui(reports): default the date range to Today
Jun 25, 2026
6a4d449
feat(ui): stepped add-relay wizard (provider → credentials → test)
Jun 25, 2026
a38d4a9
ui(relays): brand-colored monogram on the provider wizard cards
Jun 25, 2026
313c25f
ui: relabel SparkPost as Bird (rebrand)
Jun 25, 2026
f8de7fd
feat: Bird provider + richer relay test (From address, full output)
Jun 25, 2026
6ab67ff
Reject Azure sends from un-allowed MailFrom before calling ACS
Jun 25, 2026
293cdf9
Consolidate Azure MailFrom config, fix SMTP2GO false success, simplif…
Jun 25, 2026
4805ed5
Forward attachments and split To/Cc/Bcc for structured providers
Jun 26, 2026
faf0a64
SMTP conformance: Received header + loop detection, 552 on oversize, …
Jun 26, 2026
c985b76
Require TLS for SMTP AUTH by default (configurable)
Jun 26, 2026
33286ed
Generic SMTP relay: deliver to the envelope recipients, not the headers
Jun 26, 2026
4dadbec
Security audit fixes + expanded test coverage
Jun 26, 2026
702a8b5
Security hardening: ECDSA certs, 700 spool dirs, API custom-header va…
Jun 26, 2026
e0b0dc3
Add Date/Message-Id when missing (RFC 5322) before relaying
Jun 26, 2026
cd42647
Distribute a single Windows file (DispatchSetup.exe), not exe + msi
Jun 26, 2026
e67ca4b
Windows: self-contained publish so the service runs without a separat…
Jun 26, 2026
cf273e8
Windows installer: x64 package (Program Files) + pre-create logs folder
Jun 26, 2026
07498d3
CI: catch install/runtime regressions on the Actions runner
Jun 26, 2026
b32aea4
CI: real end-to-end Linux install + service smoke (mirrors Windows)
Jun 26, 2026
ee0b6b6
Add Hyper-V appliance build (Ubuntu 24.04 + SQL Express, virt-customi…
Jun 26, 2026
1e4e84a
appliance CI: install passt for libguestfs in-guest networking
Jun 26, 2026
8275a11
appliance: offline SQL install (no in-guest network / passt)
Jun 26, 2026
aa17cae
appliance: use packages-microsoft-prod.deb for the MS repo (24.04 pro…
Jun 26, 2026
fb02e50
appliance: Ubuntu 24.04 ships SQL Server 2025 (use mssql-server-2025.…
Jun 26, 2026
ba44de9
appliance: provision.sh is POSIX sh (virt-customize --run uses /bin/sh)
Jun 26, 2026
dfd5fd3
appliance: capture install.sh output during provisioning for diagnosis
Jun 26, 2026
4c8f874
appliance CI: fix boot-smoke qemu launch (KVM detect, no -cpu host un…
Jun 26, 2026
a0c3474
appliance: add UEFI fallback bootloader (\EFI\BOOT\BOOTX64.EFI)
Jun 26, 2026
0790d66
appliance CI: diagnose boot — verify fallback bootloader in image + c…
Jun 27, 2026
56bdf8f
CI: Docker build + run smoke (docker compose -> /health)
Jun 27, 2026
9cbfc4f
appliance boot smoke: explicit virtio-blk bootindex=0 + boot qcow2
Jun 27, 2026
b81b7f4
appliance boot smoke: screenshot the VGA console (firmware/GRUB outpu…
Jun 27, 2026
cde2789
appliance: move QMP screendump to a script file (fix YAML heredoc break)
Jun 27, 2026
5f52f3e
appliance boot smoke: chmod boot diagnostics readable so the artifact…
Jun 27, 2026
fa18330
appliance: explicitly enable dispatch + firstboot units (offline syst…
Jun 27, 2026
cc357bd
appliance: offline-enable via systemctl --root + relative symlinks; v…
Jun 27, 2026
350fb74
appliance: dump install.log + /opt during build verify (units missing…
Jun 27, 2026
d2a47cb
appliance: capture provision execution trace (set -x to /var/log) for…
Jun 27, 2026
5d8e7ea
appliance: run install.sh with bash -x and drop buggy rc capture (was…
Jun 27, 2026
fe91bae
install.sh: only prompt for admin password when interactive (-t 0)
Jun 27, 2026
40fb6e1
appliance: surface first-boot output on console + trace (dispatch-fir…
Jun 27, 2026
36a6d96
appliance firstboot: generate SA password from finite source (fix pip…
Jun 27, 2026
bde4896
appliance: remove debug scaffolding now that the boot smoke is green
Jun 27, 2026
7f35528
appliance: VMware OVA + KVM/Proxmox qcow2 + one-command import helpers
Jun 27, 2026
0365ae3
docs: dummy-proof deployment guide page (GitHub Pages) for every method
Jun 27, 2026
7650899
appliance: add console/SSH login (ubuntu, forced-change) + document s…
Jun 27, 2026
1c35395
appliance: bake in a dispatch-set-ip helper for easy static IP
Jun 27, 2026
1379125
docs: split the appliance guide onto its own page
Jun 27, 2026
1e73e69
docs: link the virtual appliance page from the home page
Jun 27, 2026
7513bf5
docs: recommend a static IP for the appliance instead of DHCP
Jun 27, 2026
9a6fe72
docs: refresh README + landing page with the current feature set
Jun 27, 2026
9a5b873
docs: proper documentation site (Astro + Starlight) on GitHub Pages
Jun 27, 2026
2e4579a
ci: bump actions to Node 24 majors to clear Node 20 deprecation
Jun 27, 2026
2134a8f
feat(smtp): default to ports 25 & 587, fall back to 2525 only if 25 i…
Jun 27, 2026
d7f477d
docs: make the Quickstart install-method-neutral (not Docker-led)
Jun 27, 2026
8b4417f
feat(security): portable at-rest encryption key on all platforms (Win…
Jun 27, 2026
b68354c
fix(windows+tls): keep data in ProgramData (locked down) + self-signe…
Jun 27, 2026
025971a
ci(installers): assert data-dir ACL lockdown + SMTP STARTTLS in insta…
Jun 27, 2026
4e6219b
fix(installer): WiX MSI build — XML comment can't contain '--'
Jun 27, 2026
1b3df72
feat(health): report actual listening SMTP ports alongside configured…
Jun 27, 2026
94ecb65
fix(installer): ACL lockdown must not lock out SYSTEM (Windows instal…
Jun 27, 2026
687911c
fix(appliance): require root in the KVM/Proxmox import scripts
Jun 27, 2026
05eb077
feat(appliance): accept Hyper-V Administrators (not just full admin) …
Jun 27, 2026
0e346c5
docs: note admin/root requirements for the appliance importers
Jun 27, 2026
c60a349
test: cover SMTP port fallback + encryption-key portability
Jun 27, 2026
d41bf7e
test: inbound SMTP integration (STARTTLS -> spool, CIDR reject)
Jun 27, 2026
f84f3ab
ci(appliance): verify the image binds SMTP 25 + STARTTLS in the boot …
Jun 27, 2026
d599467
chore: eliminate all build warnings (.NET + website)
Jun 27, 2026
2e91f55
ui: stop the browser offering to save relay credentials
Jun 30, 2026
895bb7a
fix(ui): Refresh on the Message Log now always fetches fresh data
Jun 30, 2026
066186d
fix(counters): denials now recorded in /stats and Reports, not just t…
Jun 30, 2026
d4ef3ef
ui: dark color-scheme so the Reports date pickers show a visible cale…
Jun 30, 2026
385e9eb
ci(installers): smoke-test SMTP allow-list denial -> log + reports
Jun 30, 2026
7d6e655
fix(message-log): collapse the list to one row per message
Jun 30, 2026
cf2d1ad
fix(message-log): group the per-API-key history to one row per messag…
Jun 30, 2026
7a51837
ci(installers): fix SMTP-deny smoke — password rejected by policy
Jun 30, 2026
d692b1e
installer(windows): tell the user where the dashboard is after Finish
Jun 30, 2026
85068ed
ci(installers): add comprehensive functional smoke as the pre-release…
Jun 30, 2026
b2a387a
ci(installers): expand functional smoke to exercise the full app surface
Jun 30, 2026
d41db39
docs: spell out HTTP API attachments (multipart `attachment`, Mailgun…
Jun 30, 2026
59cf13f
fix(purge): retention 0 means "keep forever" for log rows and spool f…
Jun 30, 2026
4965460
ci(installers): smoke tests the retention purges against real install…
Jun 30, 2026
496220f
config: default all retention windows to 7 days on a new install
Jun 30, 2026
cb3992a
feat(storage): per-retention storage usage breakdown
Jun 30, 2026
fc93c4e
ci(installers): fix routing smoke (domain match) + cover /storage
Jun 30, 2026
133fe48
fix(purge): only run the size-pressure purge on SQL Server Express
Jun 30, 2026
6387b0a
ci(installers): resolve the relative spool dir so file-purge deletion…
Jun 30, 2026
6278b71
feat(purge): archive-then-delete under Express size-pressure (weekly …
Jun 30, 2026
098f4b6
ci(installers): smoke-test size-pressure archive-then-delete (destruc…
Jun 30, 2026
5ceae26
fix(purge): nullable-clean row materialization for archiving (CI build)
Jun 30, 2026
5665357
feat(purge): configurable retention for size-pressure JSONL archives
Jun 30, 2026
1ad6b05
feat(updates): signed upgrade-bundle verifier + embedded release key
Jun 30, 2026
700681d
ci(release): build + sign per-arch upgrade bundles
Jun 30, 2026
395a45c
appliance(hyperv): guided import menu — switch picker, storage, VLAN
Jun 30, 2026
b3ebbea
appliance(hyperv): self-contained vhdx.zip with import script + READM…
Jun 30, 2026
d23e4dc
appliance(hyperv): keep VM config + disk together under <storage>\<VM…
Jun 30, 2026
41bbdac
ci(scripts): parse + lint + Pester for the PowerShell helpers
Jun 30, 2026
ce365e5
feat(updates): web-UI upload-a-package backend (verify + stage + queue)
Jun 30, 2026
15f622f
chore: strip em dashes project-wide; fix PowerShell encoding break
Jun 30, 2026
b039748
feat(updates): one cross-platform upgrade package + Linux apply mecha…
Jun 30, 2026
4706876
feat(ui): Settings -> Updates page (upload an upgrade package)
Jun 30, 2026
f0a5ba2
feat(updates): Windows apply mechanism (no MSI/WiX changes)
Jun 30, 2026
2bff936
docs: dashboard upgrade flow + upgrade-package signing-key setup
Jun 30, 2026
e7d2182
chore: set update signing key
Jun 30, 2026
c1fe182
test(updates): decouple verifier tests from the release signing key
Jun 30, 2026
88f0afa
appliance(hyperv): validate -VhdxPath is a .vhdx + clean up on failed…
Jun 30, 2026
225b9cc
appliance(hyperv): auto-find the .vhdx; storage prompt asks only the …
Jun 30, 2026
73065b5
appliance(hyperv): restore the storage-volume listing; quote updater …
Jun 30, 2026
abd3414
appliance(hyperv): detect a failover cluster and offer to make the VM HA
Jun 30, 2026
1413c41
appliance: one single-zip download per hypervisor (no nested zip)
Jun 30, 2026
57dc746
ci: build the appliance only after Build passes (no race, latest test…
Jun 30, 2026
b22867f
appliance: rename job hyperv-appliance -> build-appliances
Jun 30, 2026
1ee7f0c
version: set 0.2.0 baseline + add local upgrade-package builder
Jun 30, 2026
303610d
updates: remove local upgrade-package builder; CI owns signing
Jul 1, 2026
0cc07c2
docs: state privacy-first / no call home on homepage, docs, README
Jul 1, 2026
7fc2b68
updates: fix 500 on upgrade upload - lift request/multipart size caps
Jul 1, 2026
59f8562
updates: upload progress bar + step tracker, force re-login, upgrade …
Jul 1, 2026
15ba1d6
appliance: install hypervisor guest agents so each shows the VM IP
Jul 1, 2026
0525dfb
appliance: add the guest-agent channel in the KVM/Proxmox import helpers
Jul 1, 2026
9f70cb0
updates: reject a malformed package cleanly instead of 500
Jul 1, 2026
02c9ad1
updates: forgiving zip upload + surface the real failure reason
Jul 1, 2026
ffbcc57
fix(updates): UMask 0177 stripped dir execute bit, breaking staging
Jul 1, 2026
19b52ba
ui: auto-return to login on 401 (e.g. after a mid-upgrade restart)
Jul 1, 2026
18f4d60
security(updates): harden root applier + block downgrades
Jul 2, 2026
caa25cb
security(ci/supply-chain): pin actions to SHAs + verify base image
Jul 2, 2026
535f2c2
security(updates/auth): decompression-bomb cap + revoke sessions on p…
Jul 2, 2026
c708a4b
security: zip-slip guard on Windows apply + cap ingestion request body
Jul 2, 2026
59b530f
security: low-severity hardening batch
Jul 2, 2026
21a8c5c
installer(windows): tell users the SQL Express step takes 15-20 min
Jul 2, 2026
7effe9a
licensing: offline license-key verifier (FluxDeploy-compatible)
Jul 2, 2026
c1aa639
licensing: node-lock keys to machine id + offline revocation
Jul 2, 2026
13fd9f4
licensing: machine identity, grace-then-stop enforcement worker
Jul 2, 2026
19498a4
licensing: dashboard License page + endpoints; commercial messaging pass
Jul 2, 2026
25e8631
licensing: drop the open-source LICENSE - Dispatch is now commercial
Jul 2, 2026
a2700f7
docs: remove contributing (Dispatch is proprietary, not open to exter…
Jul 2, 2026
ace1235
docs: point downloads + security reporting at dispatchrelay.app
Jul 2, 2026
0bbb594
docs: split website out to dispatch-docs + dispatch-website repos
Jul 2, 2026
ee560b6
docs: drop stray website/.astro build cache
Jul 2, 2026
e8a9333
repo: rename dispatch-relay -> dispatch-platform
Jul 2, 2026
4ea2de5
docs: drop "open source" from the SPEC licence TOC anchor
Jul 2, 2026
c3423ef
ci: bump Node 22 -> 24 (Active LTS)
Jul 2, 2026
6c94d5d
licensing: site-wide license banner + enforcement/worker tests
Jul 2, 2026
7be55c6
Relicense to Apache-2.0 and remove all licensing enforcement
Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Keep the build context small + avoid copying host build output into the image.
**/bin
**/obj
**/node_modules
**/dist
src/Dispatch.Web/wwwroot
.git
.github
installer
docs
publish
staging
TestResults
**/*.user
.vs
.vscode
*.md
30 changes: 30 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: 2
updates:
# Keep GitHub Actions current. Third-party actions are pinned to commit SHAs (supply-chain hardening);
# Dependabot bumps those SHAs (and the trailing version comment) when new releases ship, so pinning
# doesn't mean going stale. Grouped into one PR to keep the noise down.
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
groups:
actions:
patterns: ["*"]

# Frontend dependencies (npm) for the dashboard.
- package-ecosystem: npm
directory: "/src/Dispatch.UI"
schedule:
interval: weekly
groups:
ui:
patterns: ["*"]

# .NET NuGet dependencies.
- package-ecosystem: nuget
directory: "/"
schedule:
interval: weekly
groups:
nuget:
patterns: ["*"]
180 changes: 180 additions & 0 deletions .github/workflows/appliance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
name: Appliance

# Builds the downloadable virtual appliances for ALL hypervisors in one job: customizes the Ubuntu 24.04
# cloud image with libguestfs (no Hyper-V host / nested virt needed) into a Gen2/UEFI image with SQL Server
# Express + Dispatch baked in, emits it as VHDX (Hyper-V), OVA (VMware), and qcow2 (KVM/Proxmox), then boots
# it under QEMU/UEFI to prove the service comes up on first boot. Artifacts: dispatch-appliance-hyperv,
# dispatch-appliance-vmware, dispatch-appliance-kvm.
on:
# Dispatch-only: the Build workflow kicks this off automatically AFTER its tests + lint pass (see the
# trigger-appliance job in build.yml), so the appliance is never built from un-validated code and never
# races Build. Also runnable by hand from the Actions tab.
workflow_dispatch:

env:
DOTNET_NOLOGO: "true"
DOTNET_CLI_TELEMETRY_OPTOUT: "true"

jobs:
# ONE job builds all three appliance formats (Hyper-V VHDX, VMware OVA, KVM/Proxmox qcow2) from a
# single image, then uploads them as three separate downloads near the end. The name reflects that
# it is not Hyper-V-only.
build-appliances:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: src/Dispatch.UI/package-lock.json

- name: Build + embed the web UI
run: |
( cd src/Dispatch.UI && npm ci && npm run build )
rm -rf src/Dispatch.Web/wwwroot && mkdir -p src/Dispatch.Web/wwwroot
cp -r src/Dispatch.UI/dist/* src/Dispatch.Web/wwwroot/
test -f src/Dispatch.Web/wwwroot/index.html || { echo "embedded UI missing index.html"; exit 1; }

- name: Publish self-contained (linux-x64)
run: dotnet publish src/Dispatch.Service -c Release -r linux-x64 --self-contained true -o publish/linux

- name: Install image-build tooling
run: |
sudo apt-get update
sudo apt-get install -y libguestfs-tools qemu-utils
# libguestfs's appliance must be able to read the host kernel image.
sudo chmod 0644 /boot/vmlinuz-* || true

- name: Build the appliance VHDX
env:
VERSION: ${{ github.ref_type == 'tag' && github.ref_name || format('ci-{0}', github.run_number) }}
run: |
sudo env PREBUILT_DIR="$PWD/publish/linux" VERSION="$VERSION" \
OUT="$PWD/dispatch-appliance.vhdx" OVA_OUT="$PWD/dispatch-appliance.ova" \
QCOW2_OUT="$PWD/dispatch-appliance.qcow2" \
LIBGUESTFS_BACKEND=direct ./appliance/build-appliance.sh
sudo chown "$USER" dispatch-appliance.vhdx dispatch-appliance.ova dispatch-appliance.qcow2

- name: Validate the OVA (structure, OVF XML, manifest checksums)
run: |
echo "=== tar contents (must be ovf, then mf, then vmdk) ==="
tar -tf dispatch-appliance.ova
first=$(tar -tf dispatch-appliance.ova | head -1)
case "$first" in *.ovf) ;; *) echo "ERROR: first OVA entry must be the .ovf, got $first"; exit 1;; esac
mkdir -p ova-check && tar -xf dispatch-appliance.ova -C ova-check
python3 -c "import xml.dom.minidom,glob,sys; xml.dom.minidom.parse(glob.glob('ova-check/*.ovf')[0]); print('OVF XML parses')"
( cd ova-check && while read -r line; do
f=$(echo "$line" | sed -n 's/^SHA256(\(.*\))=.*/\1/p'); h=$(echo "$line" | sed -n 's/.*= *//p')
[ -n "$f" ] || continue
act=$(sha256sum "$f" | cut -d' ' -f1)
[ "$act" = "$h" ] && echo "ok: $f" || { echo "ERROR: checksum mismatch for $f"; exit 1; }
done < ./*.mf )
echo "OVA structure + checksums valid"

# One self-contained download per hypervisor: each artifact downloads as a single zip (GitHub wraps
# it) holding that format's image + its README + helper - so the user unzips ONCE, no nested zip.
- name: Stage per-type downloads + checksums
run: |
mkdir -p out/hyperv out/vmware out/kvm
# Hyper-V: raw VHDX (GitHub compresses the artifact) + import helper + instructions.
mv dispatch-appliance.vhdx out/hyperv/
cp appliance/Import-DispatchAppliance.ps1 out/hyperv/
cp appliance/hyperv-README.txt out/hyperv/README.txt
( cd out/hyperv && sha256sum dispatch-appliance.vhdx > dispatch-appliance.vhdx.sha256 )
# VMware: the OVA + instructions.
mv dispatch-appliance.ova out/vmware/
cp appliance/vmware-README.txt out/vmware/README.txt
( cd out/vmware && sha256sum dispatch-appliance.ova > dispatch-appliance.ova.sha256 )
# KVM / Proxmox: qcow2 + import helpers + instructions.
mv dispatch-appliance.qcow2 out/kvm/
cp appliance/import-libvirt.sh appliance/import-proxmox.sh out/kvm/
cp appliance/kvm-README.txt out/kvm/README.txt
( cd out/kvm && sha256sum dispatch-appliance.qcow2 > dispatch-appliance.qcow2.sha256 )

# Upload before the boot smoke so the artifacts are available even if the smoke fails.
- name: Upload Hyper-V appliance
uses: actions/upload-artifact@v6
with: { name: dispatch-appliance-hyperv, path: out/hyperv/*, if-no-files-found: error }
- name: Upload VMware appliance
uses: actions/upload-artifact@v6
with: { name: dispatch-appliance-vmware, path: out/vmware/*, if-no-files-found: error }
- name: Upload KVM/Proxmox appliance
uses: actions/upload-artifact@v6
with: { name: dispatch-appliance-kvm, path: out/kvm/*, if-no-files-found: error }

- name: Boot smoke (UEFI) - service comes up on first boot
run: |
sudo apt-get install -y qemu-system-x86 ovmf imagemagick
code=$(ls /usr/share/OVMF/OVMF_CODE*.fd | head -1)
varsrc=$(ls /usr/share/OVMF/OVMF_VARS*.fd | head -1)
cp "$varsrc" vars.fd
echo "OVMF: $code / $varsrc"
# -cpu host requires KVM; under the TCG fallback use a TCG-compatible CPU model.
if [ -e /dev/kvm ]; then echo "KVM available"; ACCEL="-enable-kvm -cpu host"; else echo "KVM NOT available - TCG (slow)"; ACCEL="-accel tcg -cpu max"; fi
# Boot the image as qcow2 (rules out QEMU's vhdx driver - Hyper-V reads the vhdx itself) and attach
# it as an explicit virtio-blk device with bootindex=0, so OVMF (empty NVRAM) actually boots it.
# -vga std + a QMP socket let us screenshot the VGA console (firmware/GRUB output goes there, not serial).
qemu-img convert -O qcow2 out/hyperv/dispatch-appliance.vhdx boot.qcow2
# shellcheck disable=SC2086
sudo qemu-system-x86_64 -machine q35 $ACCEL -m 4096 -smp 2 -no-reboot \
-drive if=pflash,format=raw,readonly=on,file="$code" \
-drive if=pflash,format=raw,file=vars.fd \
-drive file=boot.qcow2,format=qcow2,if=none,id=hd0 \
-device virtio-blk-pci,drive=hd0,bootindex=0 \
-netdev user,id=n0,hostfwd=tcp::8420-:8420,hostfwd=tcp::2526-:25 -device virtio-net-pci,netdev=n0 \
-vga std -display none -serial file:console.log -qmp unix:qmp.sock,server,nowait -daemonize 2>qemu.err \
|| { echo "=== qemu failed to launch ==="; cat qemu.err; exit 1; }
screenshot() { # capture the VGA framebuffer via QMP -> $1 (png)
sudo python3 appliance/qmp-screendump.py qmp.sock /tmp/screen.ppm || true
sudo convert /tmp/screen.ppm "$1" 2>/dev/null && echo "captured $1" || echo "no screenshot"
}
ok=0
for i in $(seq 1 90); do # up to ~8 min (KVM boot is fast; this is mostly SQL first-run)
if curl -fsSk -m 3 https://localhost:8420/health >/dev/null 2>&1; then ok=1; break; fi
[ "$i" = 18 ] && screenshot boot-90s.png # ~90s in: capture whatever's on screen
sleep 5
done
if [ "$ok" != 1 ]; then
screenshot boot-final.png
echo "=== qemu.err ==="; cat qemu.err 2>/dev/null || true
echo "=== serial console ==="; cat console.log 2>/dev/null || echo "(serial console empty)"
echo "=== qemu still running? ==="; pgrep -f qemu-system >/dev/null && echo "(qemu running)" || echo "(qemu not running)"
# qemu (sudo) owns these root-only; make them readable so upload-artifact can collect them.
sudo chmod -f a+r boot-90s.png boot-final.png console.log qemu.err 2>/dev/null || true
echo "appliance did not serve /health within the timeout"; exit 1
fi
echo "appliance /health OK"

# Prove the appliance bound the privileged SMTP port (25, via CAP_NET_BIND_SERVICE on the systemd
# unit) and that STARTTLS negotiates with the self-signed fallback cert. Guest 25 is forwarded to
# host 2526; openssl completes a real TLS handshake (BEGIN CERTIFICATE) on success.
tls_ok=0
for _ in $(seq 1 12); do
if echo QUIT | openssl s_client -starttls smtp -connect localhost:2526 -crlf 2>/dev/null | grep -q 'BEGIN CERTIFICATE'; then
tls_ok=1; break
fi
sleep 5
done
if [ "$tls_ok" != 1 ]; then
echo "=== serial console ==="; sudo chmod -f a+r console.log 2>/dev/null || true; cat console.log 2>/dev/null || true
echo "appliance SMTP STARTTLS check failed on port 25 (forwarded to 2526)"; exit 1
fi
echo "appliance SMTP STARTTLS on port 25 OK"

- name: Upload boot diagnostics
if: failure()
uses: actions/upload-artifact@v6
with:
name: appliance-boot-diagnostics
path: |
boot-90s.png
boot-final.png
console.log
qemu.err
if-no-files-found: warn
117 changes: 117 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: Build

on:
push:
pull_request:

env:
# The actions/* @v4 run on Node 20 (deprecated); opt them into Node 24 now (forced default 2026-06-16).
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
build-test:
runs-on: ubuntu-latest

services:
# SQL Server 2022 service container (GitHub-hosted runners are free for this public repo). Same wire
# protocol / T-SQL as the dev Azure SQL Edge container; app code is unchanged. The Data test fixture
# waits up to 90s for the engine to accept connections, so no container health-cmd is needed.
sql:
image: mcr.microsoft.com/mssql/server:2022-latest
env:
ACCEPT_EULA: "Y"
# Dev-only SA password. Override by setting the MSSQL_SA_PASSWORD repo/org secret; the literal is
# only a fallback and grants nothing beyond the ephemeral CI container.
MSSQL_SA_PASSWORD: ${{ secrets.MSSQL_SA_PASSWORD || 'Dispatch_Dev_Pass123' }}
ports:
- 1433:1433

env:
# Drives the Dispatch.Data integration tests against the SQL service container.
DISPATCH_TEST_SQL: "Server=localhost,1433;User Id=sa;Password=${{ secrets.MSSQL_SA_PASSWORD || 'Dispatch_Dev_Pass123' }};TrustServerCertificate=True;Encrypt=True"
# Make the integration tests fail loudly if the connection string is ever missing, instead of skipping.
DISPATCH_REQUIRE_SQL: "1"
DOTNET_NOLOGO: "true"
DOTNET_CLI_TELEMETRY_OPTOUT: "true"

steps:
- uses: actions/checkout@v5

- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: "24"
cache: npm
cache-dependency-path: src/Dispatch.UI/package-lock.json

- name: Build React UI
working-directory: src/Dispatch.UI
run: |
npm ci
npm run build

- name: Embed UI into Dispatch.Web
run: |
rm -rf src/Dispatch.Web/wwwroot
mkdir -p src/Dispatch.Web/wwwroot
cp -r src/Dispatch.UI/dist/* src/Dispatch.Web/wwwroot/
# Fail fast if the embed produced nothing (stale/empty dist would otherwise ship a blank dashboard).
test -s src/Dispatch.Web/wwwroot/index.html || { echo "::error::Embedded UI is missing index.html"; exit 1; }

- name: Restore
run: dotnet restore Dispatch.slnx

- name: Build
run: dotnet build Dispatch.slnx --configuration Release --no-restore

- name: Test
run: dotnet test Dispatch.slnx --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --results-directory TestResults

- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
with:
name: test-results
path: TestResults/**/*.trx
if-no-files-found: ignore

# Keep em dashes out of the tree: they break Windows PowerShell parsing (a mis-decoded em-dash byte
# becomes a smart-quote string delimiter) and the project standardizes on ASCII '-'.
no-em-dash:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Reject em dashes (use ASCII '-')
run: |
matches=$(grep -rlIP '\x{2014}' . --exclude-dir=.git --exclude-dir=node_modules || true)
if [ -n "$matches" ]; then
echo "::error::Em dash (U+2014) found - replace with an ASCII hyphen '-'. Files:"
echo "$matches"
exit 1
fi
echo "OK: no em dashes."

# After a green Build on a working branch, rebuild the appliance from THIS validated commit so the image
# always has the latest tested code and never races Build. workflow_run can't be used (it only fires from
# the default branch), so we dispatch the Appliance workflow with GITHUB_TOKEN (workflow_dispatch is an
# allowed GITHUB_TOKEN trigger). `needs: build-test` gates it on success.
trigger-appliance:
needs: [build-test, no-em-dash]
if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'feat/relay-engine-web-providers') }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- uses: actions/checkout@v5
- name: Build the appliance from this validated commit
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run appliance.yml --ref "${{ github.ref_name }}"

# NOTE: tagged releases (refs/tags/v*) are handled by release.yml, which builds the signed Windows
# installer + Linux tarball and publishes a GitHub Release. This workflow only runs build + tests.
47 changes: 47 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Docker

# Builds the container image and runs the full stack (Dispatch + SQL via docker-compose), failing unless
# the service comes up and serves /health - the container equivalent of the installer smoke tests. Docker is
# native on the runner, so this needs no QEMU/nested virtualization. The release workflow builds+pushes the
# multi-arch image; this proves it actually runs before that.
on:
workflow_dispatch:
push:
paths:
- "Dockerfile"
- "docker-compose.yml"
- "src/Dispatch.Service/**"
- "src/Dispatch.Web/**"
- "src/Dispatch.Core/**"
- "src/Dispatch.Data/**"
- "src/Dispatch.Providers/**"
- "src/Dispatch.UI/**"
- ".github/workflows/docker.yml"

jobs:
build-and-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Build image + start the stack (Dispatch + SQL)
run: docker compose up --build -d

- name: Wait for the dashboard /health
run: |
ok=0
for _ in $(seq 1 60); do # up to 5 min: image already built; SQL start + DB init + service
if curl -fsSk -m 5 https://localhost:8420/health >/dev/null 2>&1; then ok=1; break; fi
sleep 5
done
if [ "$ok" != 1 ]; then
echo "=== compose ps ==="; docker compose ps
echo "=== dispatch logs ==="; docker compose logs --tail 150 dispatch || true
echo "=== sql logs ==="; docker compose logs --tail 40 sql || true
echo "dispatch /health did not respond"; exit 1
fi
echo "Docker stack /health OK"

- name: Tear down
if: always()
run: docker compose down -v || true
Loading
Loading