v26.8.0-beta.0
Pre-releaseAdded
-
Safe constrained route parameters: patterns such as
:id(\d+)match a complete decoded
segment. The parser accepts a deliberately small, bounded regex subset and rejects unsafe or
malformed expressions at boot. Specificity is now static ▸ constrained param ▸ plain param ▸
catch-all; matching remains a linear scan. -
@Headand@Optionsroute decorators, explicit-handler priority, buffered-GET HEAD fallback,
and automatic204OPTIONS responses with deterministicAllowordering. GET implies HEAD and
every existing path implies OPTIONS; streaming GET routes do not become implicit HEAD routes. -
OpenAPI route constraints and methods: constrained path params emit
schema.pattern, and
explicitly declared HEAD/OPTIONS handlers appear as operations without inventing automatic ones. -
HTML / views:
@Htmldecorator (string, file, and template modes), a zero-dep built-in
template engine ({{ }}escaped /{{{ }}}raw, exported asrender) with aviewEngine
bring-your-own hook, and zero-configstaticdirectory serving (createApp({ static: true })).
File and static serving require a filesystem (Node/Deno/Bun); string-mode@Htmlruns everywhere. -
app.fetch(request): Promise<Response>— a Web-Standards handler so the
same app runs HTTP and SSE on Deno/Bun/edge runtimes via the Fetch API. -
Deno adapter (
@green-tea/core/deno):serveDeno(app)runs HTTP + SSE + WebSocket on Deno. -
Bun adapter (
@green-tea/core/bun):serveBun(app)runs HTTP + SSE + WebSocket on Bun, reusing the neutralapp.upgrade/WsSocketcapability. WebSocket, rooms, and channels behave identically to Node and Deno. -
Cloudflare Workers / edge adapter (
@green-tea/core/edge):edgeHandler(app)runs HTTP + SSE + WebSocket on workerd, reusing the neutralapp.upgrade/WsSocketcapability. Requires thenodejs_compatcompatibility flag. Green Tea now runs on Node, Deno, Bun, and the edge — with identical WebSocket, rooms, and channel behaviour on all four. -
app.upgrade(request, socket): neutral WebSocket entry point for non-Node runtimes, built on a sharedWsSocketcapability. WebSocket logic is now runtime-agnostic (src/http/ws-core.ts). -
Mesh (alpha) runs on Node, Deno and Bun — teapot and teacup, in any combination
(a Deno teapot can serve a Node teacup). It no longer needsapp.listen(): the graph boots on
first use, soserveDeno/serveBunwork throughapp.fetch/app.upgrade. Edge is not
supported — the teapot's secret comparison needsnode:crypto'stimingSafeEqual, which
nodejs_compatdoes not provide. -
MESH_PROTOCOL_VERSION: the mesh wire is versioned. Peers exchange it in thehello/manifest
frames and refuse a mismatch, naming both versions, instead of misreading each other's frames.
The teapot checks the version before the secret — a skewed peer is not an auth failure. -
HttpErroracceptsheaders, so a custom error can carry its own response headers
(retry-after,etag, …) without a special case in the error renderer. -
app.ready(): Promise<void>— resolves the dependency graph and returns. On a mesh app it
connects the teapots and splices their scopes in; on every other app it is a no-op, so
await app.ready()beforeinspect()/graph()/explain()works against either kind without
knowing which you were handed. It does not boot providers: resolving the graph and being
ready to serve are different things, and drawing a diagram should not open your database
connections. Serving boots them too and shares the same memoized step. -
Mesh heartbeat (
mesh.heartbeatMs, default 15s): each teacup pings its teapots and closes a
link after two unanswered rounds, so a half-open connection surfaces as an immediate 503 rather
than every request payingtimeoutMsfirst. Ping/pong are mesh frames, not WebSocket protocol
pings — the platformWebSocketon Deno/Bun does not exposews.ping().
Fixed
- The Deno WebSocket adapter snapshots request and connection metadata before accepting an upgrade;
Deno 2.9 invalidates that metadata once upgraded, which previously broke WebSocket and mesh boots. - Repeated slashes and malformed path encoding now return
400consistently across Node and Fetch
adapters, retaining configured security/CORS headers./pathand/path/remain equivalent. - Ambiguous same-method route shapes now fail at boot with both declarations named. Effective-shape
checks also cover remote mesh conflicts and local routes that shadow a remote export. - HEAD responses always suppress the body while preserving handler status and headers; Fetch
responses also avoid constructing forbidden bodies for204,205, and304statuses. - The opt-in dev routes
/__graph__(graph viewer) and/__openapi__are now
served overapp.fetchtoo, so they work on every runtime (Deno/Bun/edge),
not only the Nodeapp.listen()path. - A teapot with a live control channel could never shut down. Mesh control connections were
not registered with the stream registry, soserver.close()waited on a connected teacup that
had no reason to hang up, andapp.close()never resolved. - A downed teapot now answers 503 immediately instead of hanging for the full
timeoutMs
(30s by default) and then answering 500. A closed socket cannot deliver the frame, so the wait
bought nothing. An RPC that times out on a live link is now 504, not 500 — a dead upstream and
a slow one are different operational stories, and neither is "this service broke". request:step:enter/leaveare now emitted for@Wsand mesh routes. Only HTTP routes
emitted them, so a logging plugin silently observed nothing on a WebSocket route — a gap in a
documented plugin API. All transports now run their steps through one path.- A mesh route exported by two teapots now fails the boot, naming both effective patterns and
both teapots, instead of silently serving whichever connected first and leaving the other dead.
There is no load balancing to fall back on, so green-tea will not pick for you. - A local route shadowing a remote one now warns. Local still takes precedence — that is how you
override a teapot — but a silently shadowed export used to look like a broken teapot. app.close()closes mesh links even with no server, so a mesh app booted throughapp.fetch
(every Deno/Bun deployment) no longer leaks its teapot connections.- WebSocket frames arriving during boot are no longer dropped.
app.upgradeawaited the boot
before handing the socket to a consumer, and the inbound channel is fan-out, so a peer that spoke
first lost those frames — for mesh, that was the handshake itself. - The plugins guide documented a
request:step:exitevent that has never existed; the bus emits
request:step:leave.
Changed
- Root, runtime-only, and website dependency audits are clean after supported package updates and
narrow pins for vulnerable transitives. CI audits root + website trees and builds the docs; the
GitHub OIDC release workflow audits immediately before its publish gate. - App-scope providers now boot exactly once (memoized): a second
app.listen()
call no longer re-runs provider factories or their side effects. WsOpenCtx.req(available in@Ws/@Ssehandlers) is now a neutral
WsRequest({ url, headers, protocol, ip }) instead of the Node
http.IncomingMessage, so it works the same across Node and Deno. Node-only
fields such asreq.socket/req.rawHeadersare no longer available on
ctx.req; usectx.protocol/ctx.ip/ctx.query/ctx.headers
instead — all still provided.- Breaking (pre-1.0): transport is now enforced by declaration. A buffered route
(@Get/@Head/@Post/@Put/@Patch/@Delete/@Options) whose handler returns an
AsyncIterable, or a streaming route (@Sse/@Ws) whose handler returns a plain value, now
fails with a 500TransportMismatchErrorinstead of silently switching behavior.@Stream
still negotiates both. Declare@Sse/@Stream/@Wsto stream — a return value no longer
changes a route's wire contract.