fix(inbound): host-gate SEO routes to the posternonline.com zone (v1.4.4) - #580
Conversation
…4.4) /robots.txt and /sitemap.xml hardcode the demo's canonical origin and were served on every host. Postern is self-hostable, so an operator deploying on their own mail domain served a robots.txt/sitemap.xml advertising the Skyphusion Labs demo instead of their own mailbox. redirectApexToDemo already host-gates the same zone; these two did not. Gated on a shared isPosternonlineZone() predicate (extracted from the apex-redirect and demo-landing checks) so a non-demo host falls through to the ordinary not_found path, restoring exactly what production served on v1.4.3. Bundled in the same release: - serveSitemap computed <lastmod> from new Date() at request time, so every crawl read "modified today" forever. Replaced with a content-derived constant. - GET /health had no version field, so the doctrine of verifying the live Worker artifact (not the deploy pipeline) had no wire answer. Single-sourced in inbound/src/version.ts, guarded by a test against package.json, mirroring the MCP advertised-version fix (mcp/#573). - .github/workflows/codeql.yml failed its javascript-typescript leg on every push: actions/setup-node with cache: npm cannot find a root lockfile (none exists; lockfiles live at inbound/, mcp/, webmail/e2e/). Both matrix legs run build-mode: none, so CodeQL never needed node_modules; dropped the unneeded setup-node/install steps rather than teaching them about three lockfiles. - Removed the CHANGELOG "Unreleased" block describing the MCP version fix: it violated the file's own stated policy that postern-mcp-v* tags get no section here (npm is that train's ledger), and its "stuck at 1.3.0" text described the source at the time, not anything ever published. Version pins landed for v1.4.4: inbound/package.json (+ package-lock.json), clients/python/pyproject.toml, clients/python/postern_client/__init__.py, and a CHANGELOG.md ## v1.4.4 section, per the shared tag-preflight gate. Closes #579 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial security auditGenerated 2026-08-07T23:08:33.337Z · PR hardens SEO host-gating and adds version endpoint; no authz/injection/SSRF/secrets/data-leak vulnerabilities introduced.
|
Code Coverage OverviewLanguages: JavaScript JavaScriptThe overall coverage in commit 86284e7 in the Show a code coverage summary of the most impacted files.
Updated |
The lockfile-cache fix alone (previous commit) only revived the javascript-typescript leg. #577 added this advanced workflow with a two-language matrix (actions, javascript-typescript) that silently displaced GitHub's default CodeQL setup, which was analyzing FOUR languages through 2026-08-06 (confirmed via the code-scanning analyses list: analysis_key dynamic/github-code-scanning/codeql:analyze for actions, javascript-typescript, go, and python). Go (relay/) and Python (imap/, clients/python/) were never in the advanced matrix at all, so real coverage had silently collapsed to one language while the check still reported green -- a worse state than visible red, because it looks fixed. Widened the matrix to all four languages the repo actually contains, matching what default setup was demonstrably analyzing: - go does not support build-mode: none (compiled language; verified against GitHub's build-mode support docs). Uses autobuild instead -- relay/ is an ordinary Go module, no manual build step needed. Added actions/setup-go@v7 (go only), tracking relay/go.mod the same way ci.yml's own relay job does so CodeQL never drifts from the toolchain the code actually builds with. - python supports build-mode: none like javascript-typescript and actions; no additional setup step needed. - the existing javascript-typescript-only setup-node/install guard is unaffected by the wider matrix: it is a positive match on that one language value, so go and python never touch it. - added the autobuild step, gated on matrix.build-mode == 'autobuild', so it only runs for the go leg. Did not change the workflow name: or the job name: template (org ruleset binds required status checks to those names). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… codeql.yml comment Two things, both requested by the lead after reviewing b64a1ad. 1. Corrects the header comment in .github/workflows/codeql.yml that commit b64a1ad landed. That comment claimed real CodeQL coverage had "collapsed to ONE language (actions)" -- wrong, and the lead's own error, not something this correction blames on the prior read. The code-scanning/analyses API defaults to the default branch; reading main-only data read as an unanalyzed repo. GitHub's default setup was, and is, still enabled and still analyzes all four languages on pull requests (provable on postern#580 itself, a separate dynamic/github-code-scanning/codeql run passing go and python). What actually stopped was PUBLICATION TO refs/heads/main specifically: this workflow's push-to-main trigger, once added, left go/python with no matrix row and no publisher, and the javascript-typescript leg died in setup-node before uploading either. So main's stored results for three languages went stale while PR-time analysis kept covering all four -- a publication gap on the default branch, not an unanalyzed repo. History is not rewritten; this is the follow-up correction commit, referencing b64a1ad by sha. 2. Bumps the transitive nanoid dependency to 3.3.18 (>=3.3.17 clears GHSA-2v37-7h3g-55p8, a high-severity advisory: custom generators can loop indefinitely when size is zero) in inbound/package-lock.json and mcp/package-lock.json via `npm audit fix`. Verified: main and this branch both pinned nanoid 3.3.16 identically before this commit, so the advisory is newly disclosed, not introduced by this PR's earlier lockfile refresh -- but it fails the required `ci` check either way and main is equally exposed, so fixing it here rather than leaving a known-red gate. nanoid arrives via vitest -> vite -> postcss, a dev-only chain that never reaches the deployed Worker or the published postern-mcp package. Confirmed the fix touched only nanoid's lockfile entry in both packages (no other package version dragged); neither package.json changed, and mcp/package.json's own version and mcp/src/version.ts are untouched -- this is not the MCP republish, which stays its own postern-mcp-v* tag track. CHANGELOG.md ## v1.4.4 gains a line for each. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mackaye (skyphusion-mackaye)
left a comment
There was a problem hiding this comment.
Approving. Reviewed against the artifacts rather than the report; every claim below I re-derived myself.
The blocker (#579) is correctly fixed. isPosternonlineZone is a single definition of the zone boundary and serveDemoLanding now shares it instead of carrying an inline copy. endsWith(".posternonline.com") requires the literal dot, so evilposternonline.com does not match. Leaving redirectApexToDemo on its own narrower apex/www check was the right call and not an oversight: folding it into the zone predicate would have made demo.posternonline.com redirect to itself.
The test discriminates the defect. Both directions are asserted, and the negative control was run: reverting only the two gate conditions turned the self-hoster test red at the exact assertion (expected 200 to be 404), then restored. A guard nobody has watched fail is not yet a guard; this one has been watched.
Verified independently by me:
- nanoid is the ONLY changed package in either lockfile. I diffed every entry in
inbound/andmcp/against b64a1ad: one version change each, nothing added, nothing removed. - CodeQL run 31225785760 produced four
Analyze (...)legs, all green, and I grepped the full run log for both SARIF-rejection signatures: zero matches. npm auditpasses ANDtypecheck/testshowsuccessrather thanskippedin both jobs. On every earlier head the audit gate failed first and those steps never ran, so this is the first head where CI actually executed the suite.- All five version pins agree at 1.4.4;
release-gategreen.
Merging with a merge commit, not a squash. Squashing would collapse b64a1ad and its correction into one commit and destroy the "this corrects b64a1ad" relationship, which is the thing that makes the record auditable. Ancestry is not evidence of preservation, so that structure has to survive deliberately.
Two things here were the author's judgment, not the dispatch, and both are the reason this is a good release:
The CHANGELOG header stating no PROJECTION_VERSION / POSTERN_IMAP_UIDVALIDITY bump and that door rolls from this tag are image-refresh only. That interlock turned a routine roll into a production incident on this repo eight days ago. An operator reading the tag needs exactly that sentence and nobody asked for it.
The setup-go pin to relay/go.mod, so CodeQL cannot drift from the toolchain ci.yml builds relay with, plus checking GitHub's docs to establish that go rejects build-mode: none rather than assuming it.
One correction that is mine, recorded here so the PR reads honestly: the "coverage collapsed to one language" framing in b64a1ad came from me and was wrong. I read code-scanning/analyses, which defaults to the default branch, and generalised a main-only observation to the whole repo; PR-time analysis had been covering all four languages throughout. This PR's own CI is what disproved it. The follow-up commit corrects the characterisation without rewriting history, which is the right shape.
Closes #579
Why (per task)
1. Host-gate the SEO routes (the blocker).
/robots.txtand/sitemap.xml(inbound/src/api.ts) hardcode
DEMO_CANONICAL_ORIGIN = "https://demo.posternonline.com"and were served on EVERY host. Postern is aself-hostable product (DEPLOY.md); an operator deploying on their own mail
domain served a robots.txt/sitemap.xml advertising the Skyphusion Labs demo
instead of their own mailbox.
redirectApexToDemoalready host-gates the samezone; these two did not. Fix: extracted a shared
isPosternonlineZone(hostname)predicate (apex or any
*.posternonline.comsubdomain), built fromisPosternonlineSubdomain()whichserveDemoLandingnow also uses instead ofits own inline
endsWithcheck, so there is one definition of the zoneboundary instead of three. Off the zone, both routes fall through to the
ordinary
not_foundpath (noisApimatch for/robots.txt//sitemap.xml),restoring exactly what v1.4.3 served (verified live before this change:
postern.skyphusion.org/robots.txtwas 404).2. Sitemap lastmod.
serveSitemapcomputednew Date()at request time, soevery crawl read "modified today" forever and crawlers discount that signal.
Replaced with
SITEMAP_LASTMOD, a content-derived constant bumped only whenthe two listed pages (landing, webmail) actually change.
3. Version on /health. The wire had no way to answer "confirm the live
Worker version" (this repo's own doctrine: verify the artifact, not the
pipeline). Added
inbound/src/version.ts(a guarded literal, matchingmcp/src/version.ts's shape exactly, including why it is not apackage.jsonimport:tsconfig.json'sincludescopes tosrc/**/*.ts,so importing above
srcbreaks the build) plusinbound/version.test.tsasserting it equals
inbound/package.json.GET /healthnow returns{"ok":true,"service":"postern","version":"1.4.4"};HEAD /healthbehavior(status/headers match GET, no body) is untouched -- covered by the existing
test. Updated
docs/OPERATIONS.mdto describe the new field.4. CodeQL workflow -- fixed AND widened to 4-language coverage (revised
after initial review). The lockfile fix alone (first commit) only revived
javascript-typescript. Mackaye pulled the code-scanning analyses list keyedby
analysis_keyand found that.github/workflows/codeql.yml(#577) hadsilently DISPLACED GitHub's default CodeQL setup, which was analyzing FOUR
languages through 2026-08-06:
dynamic/github-code-scanning/codeql(default setup) was coveringactions/javascript-typescript/go/python; the advanced workflow's two-language
matrix, with
javascript-typescriptadditionally dying atsetup-nodebefore upload, left real coverage at ONE language (
actions) while thecheck still read green -- worse than visible red, because it looks fixed.
go(relay/) andpython(imap/, clients/python/) had never been in theadvanced matrix at all.
Fix (second commit,
b64a1ad): widened the matrix to all four languages.Verified rather than assumed the two open questions:
that Go does NOT support
build-mode: none(compiled language) --requires
autobuildormanual.relay/is an ordinary Go module, soautobuild(gatedif: matrix.build-mode == 'autobuild') needs no manualbuild step. Added
actions/setup-go@v7(go leg only) trackingrelay/go.mod, mirroring howci.yml's ownrelayjob pins itstoolchain, so CodeQL's Go extraction never drifts from what the code
actually builds with.
pythonsupportsbuild-mode: nonelike theexisting two languages -- no extra setup step.
if: matrix.language == 'javascript-typescript'guard on setup-node/install is a positive matchon that one language value, so it does not fire for the new
go/pythonmatrix entries -- confirmed live (see leg-by-leg result below), not just
read from the YAML.
Did not change the workflow
name:or jobname:template (org rulesetbinds required status checks
coverage/CodeQL/cito those names).Verified in real CI at PR head
b64a1ad(thepull_request-triggeredrun of this workflow, not the separate GitHub-default dynamic re-check that
also fired): exactly 4
Analyze (...)legs, allsuccess:and the aggregate
CodeQLrequired status check is green. Leg count andlanguage names are the acceptance criterion here, not just "the check is
green" -- a two-leg green run is the exact failure mode this fixes.
5. Version pins + CHANGELOG. Landed all four pins the shared
tag-preflight.shasserts at 1.4.4:inbound/package.json(+package-lock.json, refreshed vianpm install --package-lock-only, nodependency changes),
clients/python/pyproject.toml,clients/python/postern_client/__init__.py, and aCHANGELOG.md## v1.4.4section. Also removed the
## Unreleasedblock describing the MCPadvertised-version fix:
CHANGELOG.md's own preamble sayspostern-mcp-v*tags "get no section here" (npm is that train's ledger), andthe block's "stuck at 1.3.0" text described the source at the time (the fix
already merged to
mainin #573/#578), not anything ever published. Didnot touch
mcp/package.jsonormcp/src/version.ts-- that republishrides its own
postern-mcp-v*tag track and its own PR.npm run routes:emitre-run in the same commit;contracts/api-routes.jsonpicked up the updated
note:on therobots/sitemaprows stating the zonegate (
contracts/api-params.jsonunchanged -- neither route takesparameters).
Gate (task 1-3/5, run against
fc9b59d; task 4 gate below againstb64a1ad)Task 4 gate:
.github/workflows/codeql.ymlhas no local runner (it onlymeans anything as live GitHub Actions), so it was verified by pushing and
reading the real run -- see the leg-by-leg result above.
Repo-wide em-dash/en-dash scan (
git ls-files -z | xargs -0 grep -lP '[\x{2013}\x{2014}]') over the full tracked tree, run again afterb64a1ad:zero hits.
Negative control (task 1, required)
Reverted only the
isPosternonlineZone(url.hostname)conditions on the tworoute guards (api.ts unchanged otherwise), reran
api.test.ts:Restored the host gate, reran: 11/11 green (see gate above, full suite
815/815). The test does observe the defect it exists to guard.
Notes / self-disclosure
used the Edit tool directly on
docs/OPERATIONS.md, and it landed in theshared
/home/conrad/dev/posterntree instead of my own clone via my ownshell -- a violation of the file-tools rule (Edit runs as the parent
process identity; wrong tree entirely, not even my clone). Caught it via
git diff/git statusin that tree, rangit checkout --there torevert (confirmed clean after), redid the identical edit correctly in my
own clone via
python3undersudo -u albini.the new
codeql.ymlcontent with an unwrappedcp/rmoutside thesudo -u albinistring. It landed in/home/mackaye/dev/postern-- whichturned out to be a symlink (
~mackaye/dev -> /home/conrad/dev, set2026-07-27) to the SAME shared tree as error Inbound email -> agent-friendly worker (CF Email Routing + D1 + Vectorize, lean v1) #1, reached via a path I
hadn't accounted for. Caught it the same way (
git status/git diffshowed the stray change under the lead's own identity), reverted with
git checkout --there (confirmed clean), and redid the copy correctlyentirely inside one
sudo -u albini bash -lc '...'invocation. Neitherslip was committed, pushed, or left any trace outside the working tree;
no secret exposure either time. Flagging both plainly rather than
smoothing them over.
nanoidaudit finding was fixed in round 2 (see the sectionbelow) after the lead reviewed the triage and asked for it to be
folded into this release rather than left red. Original triage stands: it
was newly-disclosed and pre-existing, not introduced by this PR.
will not.
Round 2 (after further review: codeql comment correction + nanoid fold-in)
Two corrections from the lead, both landed in commit
86284e7(new head).1. Corrected the
.github/workflows/codeql.ymlheader comment. Myearlier commit (
b64a1ad) carried a claim from the lead's initial findingthat real CodeQL coverage had "collapsed to ONE language (actions)" -- WRONG,
and the lead's own error, which he corrected and I'm relaying precisely
rather than softening: the
code-scanning/analysesAPI defaults to thedefault branch, so reading main-only data read as an unanalyzed repo.
GitHub's default setup was, and is, still enabled and still analyzes all
four languages ON PULL REQUESTS -- provable on this very PR (a separate
dynamic/github-code-scanning/codeqlrun passes go/python alongside our ownworkflow's run). What actually stopped was PUBLICATION TO
refs/heads/mainspecifically: the last default-setup upload for javascript-typescript/go/
python against main was 2026-08-06T04:41Z, and since 08-07 (when #577 added
this advanced workflow) only
/language:actionspublished there, becausethis workflow also runs on push to main with no row for go/python and a
javascript-typescript leg that died before uploading. So main's STORED
results for three languages went stale while PR-time analysis kept covering
all four -- a publication gap on the default branch, not an unanalyzed repo.
Rewrote the comment to say exactly that, referencing
b64a1adby sha ratherthan rewriting its commit message (history stays as it is; this is the
follow-up correction).
2. Bumped transitive
nanoidto 3.3.18 (satisfies>=3.3.17, clearsGHSA-2v37-7h3g-55p8) in
inbound/package-lock.jsonandmcp/package-lock.jsonvianpm audit fix. Verified before pushing that itdragged nothing else:
git diffon both lockfiles shows only thenanoidentry's
version/resolved/integritychanged (3 lines each), and bothpackage.jsonfiles are byte-identical to before.nanoidarrives viavitest -> vite -> postcss, a dev-only chain; never reaches the deployedWorker or the published
postern-mcppackage. Did not touchmcp/package.jsonormcp/src/version.ts(per the lead's fence -- MCPrepublish stays its own tag track). Added two lines to
CHANGELOG.md## v1.4.4: the widened CodeQL coverage (previously unrecorded) and thissecurity bump with its advisory id.
Acceptance criteria (as specified, not just "CI is green")
At head
86284e7:ci,inbound,mcp: all success. Confirmed by job step, not justjob conclusion -- both
inboundandmcpshowRun npm run typecheckandRun npm testas individual green steps (mcp alsoRun npm run build),none
skipped.npm audit (dependency vulnerability gate)also green inboth,
npm audit --audit-level=highre-run locally in both packages:found 0 vulnerabilities.pull_requestrun produced exactly 4Analyze (...)legs --actions,javascript-typescript,go,python-- all
success. Checked the raw job logs for the SARIF-rejection failuremode the lead flagged ("CodeQL analyses from advanced configurations
cannot be processed when the default setup is enabled"): not present in
either the
goorpythonleg. Both logs showSuccessfully uploaded results/Analysis upload status is complete.Full local gate re-run after round 2 (inbound typecheck + 815/815 vitest,
mcp typecheck + 85/85 vitest + build, both
npm auditclean) -- all green,matching CI.
Nothing in flight. Ready for review; have not merged and will not.