Skip to content

feat(sdk): testing utilities, a real logger, and docs an agent can install from - #32

Merged
cport1 merged 1 commit into
mainfrom
feat/dx-testing-logging
Aug 22, 2026
Merged

feat(sdk): testing utilities, a real logger, and docs an agent can install from#32
cport1 merged 1 commit into
mainfrom
feat/dx-testing-logging

Conversation

@cport1

@cport1 cport1 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #729, #734 and #735 — the DX batch.

#734 — testing utilities

The SDK has hundreds of tests and a customer had none of them. There was no supported way to write "assert this request would be denied" against your own rules, so the first time anyone learned what the middleware does to their traffic was in production. That is also why mode: 'monitor' had to become the default.

import { createTestHarness, get, expectDenied, expectAllowed } from '@webdecoy/node/testing';

const wd = createTestHarness({ rules: [tripwire()] });
expectDenied(await wd.protect(get('/.env')), { rule: 'tripwire' });

Three things made that test hard to write by hand, and each is why a helper exists:

  • Offline by default. A WEBDECOY_API_KEY in CI would otherwise turn every unit test into a live network call — slow, flaky, and it files test traffic as real detections in the customer's dashboard. allowNetwork: true opts in. There's a test asserting fetch is never called.
  • Isolated state. Each harness gets fresh in-memory stores, so rate-limit counters don't leak between cases and the second test to run doesn't fail for reasons belonging to the first.
  • Failure messages that name the rule. expectDenied prints every rule and its state. "expected false to be true" tells you nothing about which of six rules was supposed to fire.

Shipped as a /testing subpath export so none of it reaches production bundles. protectMany() runs a rate limit to its edge without sleeping.

#735 — logging

Was debug: booleanconsole.log: either off or noise, unroutable into the app's own logger, unsampleable, unstructured.

new WebDecoy({ logger: myLogger });
new WebDecoy({ logger: fromPino(pino()) });

One deliberate behaviour change: warnings and errors are no longer gated on debug. A violation that failed to report, or a rejected key, is something the operator needs whether or not they opted into diagnostics.

fromPino() exists rather than argument-order autodetection. Pino takes (fields, message) — the reverse of this interface and of console — so passing one directly type-checks and then silently drops every structured field. One explicit wrapper beats guesswork that's wrong for somebody.

I also wrote and then deleted a resolveLogger() doc comment claiming it detected argument order, which it didn't. Flagging it because the fix was to make the code match the doc, not the other way round.

#729 — docs an agent can install from

llms.txt and AGENTS.md. Coding agents install dependencies now and the repo gave them nothing to read — docs/ had exactly one file. This matters more for us than for competitors: our install path is the activation problem, and an agent that can install and verify without a human reading docs is the shortest version of it.

Most of AGENTS.md is what not to do, because those are the expensive mistakes: don't enable enforce on a first install, don't invent an API key, don't add filter() without one, don't leave a proxied app on the default trustProxy, don't describe attackSignatures() as a WAF. It also insists the agent run the WebDecoy-Test/1.0 one-liner and report what it returned — an install isn't finished until something visible happened.

Both are public-safe: no metrics, no incidents, no internal specifics.

A real bug the docs caught

Writing the "how do I read a verdict" section surfaced an inconsistency I'd introduced in #31: req.webdecoy is the detection in the Node adapters, while Hono's c.get('webdecoy') was the decision. I'd written the doc claiming they were the same.

Fixed in the code rather than the doc: all four adapters now also expose webdecoyDecision, which means one thing everywhere. req.webdecoy keeps its existing meaning. Dropping the as any casts that came with it lowered the budgets: express 12→10, nextjs 3→2.

Verification

24 new tests, 430 total, 20/20 turbo tasks, check:edge green on all three entry points.

…stall from

Three things a developer notices in the first ten minutes, none of which
the SDK had.

TESTING. We have hundreds of tests and a customer had none of them. There
was no supported way to write "assert this request would be denied"
against your own rules, so the first time anyone learned what the
middleware does to their traffic was in production -- which is also why
monitor had to become the default mode. createTestHarness() is offline by
default: a WEBDECOY_API_KEY in CI would otherwise turn every unit test
into a live call and file test traffic as real detections in the
customer's dashboard. Each harness gets its own rule state, so rate-limit
counters do not leak between cases and the second test to run does not
fail for reasons belonging to the first. Assertion failures print every
rule and its state, because "expected false to be true" says nothing about
which of six rules was supposed to fire.

LOGGING. It was `debug: boolean` writing to console.log -- either off or
noise, unroutable, unsampleable, unstructured. Now any object with
debug/info/warn/error. Warnings and errors are no longer gated on debug: a
violation that failed to report is not diagnostic output. fromPino()
exists because pino's argument order is reversed, and passing one directly
type-checks and then silently drops every field.

DOCS FOR AGENTS. Coding agents install dependencies now and the repo gave
them nothing to read. llms.txt and AGENTS.md are written for that reader,
and most of AGENTS.md is what NOT to do -- do not enable enforce on a
first install, do not invent an API key, do not leave a proxied app on the
default trustProxy, do not call attackSignatures() a WAF.

Writing them caught a real inconsistency: req.webdecoy is the detection in
the Node adapters while Hono's c.get('webdecoy') was the decision. All
four now also carry webdecoyDecision, which means one thing everywhere.
Removing the `as any` casts that went with it dropped express 12->10 and
nextjs 3->2.

Closes WebDecoy/app#729
Closes WebDecoy/app#734
Closes WebDecoy/app#735
@cport1
cport1 merged commit a69e4a4 into main Aug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/dx-testing-logging branch August 22, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant