Skip to content

feat: tripwire challenge/log, UTC timestamps, and a safe release script - #68

Merged
cport1 merged 1 commit into
mainfrom
fix/53-55-and-release-tooling
Jul 30, 2026
Merged

feat: tripwire challenge/log, UTC timestamps, and a safe release script#68
cport1 merged 1 commit into
mainfrom
fix/53-55-and-release-tooling

Conversation

@cport1

@cport1 cport1 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #53, closes #55.

#53 — tripwires can Challenge or Log

block_action has always accepted challenge and log; tripwire_response — the path backed by proof rather than a score — had only the harsher options. Both added, with the UI copy.

log records and returns. Worth knowing it is not the same as tripwire_dry_run, which collapses to ALLOW inside the rule (TripwireRule.php:139) before RuleEngine can record it — dry run is silent, this is observable.

challenge serves the existing PoW interstitial. Documented rather than papered over: it binds to a click and needs JS, so nothing automated can complete it. On a tripwire that is the intent, but it is still a denial for any non-JS caller, which is why block stays default.

Deliberately not gated on GoodBotList::verifyBotIP() or isGoodBot() — the earlier review found both return true on User-Agent alone for bots they have no verification rule for, so either would be a spoofable bypass.

#55 — UTC storage

All eight created_at writers store UTC; the one display site converts via get_date_from_gmt(). They were site-local while readers built bounds with gmdate(), so on any non-UTC site the Today/7d/30d filters covered the wrong span and the checkout window was the wrong width.

Existing rows are not migrated. Shifting by the current offset is wrong for any site that has ever changed timezone, and a bad shift is silent and unrecoverable — whereas the mixed-epoch skew is bounded and ages out (1h for checkout attempts, the reporting window for detections). Reasoning recorded at the canonical writer.

Release tooling — the actual point of this PR

bin/deploy-wporg.sh now refuses to commit if trunk carries anything wp.org forbids, and asserts the staged version matches. Not hypothetical: syncing from a build/ tree left by release.sh stages includes/class-webdecoy-updater.php (a self-updater pulling from our CDN — grounds for removal from the directory), and rsync had already carried a local .claude directory in. Both were caught by eye; now the script catches them.

New bin/release-all.sh does both channels in the only safe order. The two variants overwrite each other's output and the ZIP is not byte-reproducible — mtimes land in the archive, so a rebuild changes the sha256. update-info.json must ship from the same pass that produced the zip it names or the self-hosted updater rejects the download on a hash mismatch. CDN first (verify hash → upload zip → upload manifest → verify what the CDN serves), then wp.org. Preflight refuses a dirty tree, a non-main branch, drift from origin, or a version not declared consistently in all three files.


80/80 tests, phpcs baseline unchanged (14, all pre-existing in tests/bootstrap.php), phpstan unchanged.

…at can't repeat my mistakes (#53 #55)

#53 — tripwire_response accepts challenge and log. block_action has always
accepted both; the deterministic path, the one backed by proof rather than a
score, had only the harsher options. `log` records and returns — note this is NOT
the tripwire_dry_run setting, which collapses to ALLOW inside the rule
(TripwireRule.php:139) before RuleEngine can record it, so dry run is silent where
this is observable. `challenge` serves the existing proof-of-work interstitial.

Documented rather than papered over: the challenge binds startChallenge to a click
and needs JavaScript, so no automated client can ever complete it. On a tripwire
that is the intent — the path exists nowhere and is reachable only from a hidden
element or a robots disallow — but it is still a denial for any non-JS caller,
which is why block stays the default. Deliberately did NOT gate it on
GoodBotList::verifyBotIP() or isGoodBot(): both return true on User-Agent alone
for bots they have no verification rule for, so either would be a trivially
spoofable bypass.

#55 — all eight created_at writers now store UTC, and the one display site
converts with get_date_from_gmt(). They were current_time('mysql') (site-local)
while readers built bounds with gmdate() (UTC), so on any non-UTC site the
Today/7d/30d filters covered the wrong span and the checkout velocity window was
the wrong width — narrower west of UTC, wider east. Existing rows are NOT
migrated: shifting by the current offset is wrong for any site that has changed
timezone, and a bad shift is silent and unrecoverable, whereas the mixed-epoch
skew is bounded and ages out (one hour for checkout attempts, the reporting window
for detections). The reasoning is recorded at the canonical writer.

Release tooling. bin/deploy-wporg.sh now refuses to continue if trunk carries
anything WordPress.org forbids, and asserts the staged version is the one
requested. This is not hypothetical: syncing from a build/ tree left behind by
release.sh stages includes/class-webdecoy-updater.php — a self-updater fetching
from our own CDN, an outright guideline violation — and rsync had already carried
a local .claude directory into trunk. Both were caught by eye before commit; now
they are caught by the script.

New bin/release-all.sh does both channels in the only safe order, because the two
build variants overwrite each other and the ZIP is not byte-reproducible (mtimes
land in the archive). update-info.json must therefore ship from the same pass that
produced the zip it names, or the self-hosted updater rejects the download for
every Pro install on a hash mismatch. CDN first (build, verify hash, upload zip
before manifest, verify what the CDN serves), then wp.org, which rebuilds --org
itself. Preflight refuses a dirty tree, a non-main branch, a branch out of sync
with origin, or a version not declared consistently across webdecoy.php,
readme.txt and changelog.txt.

80/80 tests, phpcs baseline unchanged, phpstan unchanged.
@cport1
cport1 merged commit 8ead475 into main Jul 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant