feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

constguard=createFetchGuard({mode: 'enforce',rules: [tripwire()]});exportdefault{asyncfetch(request: Request): Promise<Response>{const{ response }=awaitguard.check(request);if(response)returnresponse;returnguard.decorate(awaithandle(request));},};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*',webdecoy({rules: [tripwire()],skipPaths: ['/health']}));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.
createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.
Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.
Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.
check:edge now covers three entry points.
ClosesWebDecoy/app#727ClosesWebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for freeto 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

@cport1