{"build":"75e26b10e","features":["b1.catalog","b1.wsdl.https","b1.gql.http-errors","b1.gql.ws","b1.gql.ws.strict","b1.accesslog","b2.gql.schema.types","b2.gql.subs","b2.gql.http","b2.gql.schema.fields","b3.ws.handshake","b3.ws.frames","b3.ws.close","b3.ws.deflate","b4.sse.parser","b4.sse.transport","b4.sse.streams","b4.sse.ai","b5.soap.version","b5.soap.wsdl","b5.soap.faults","b5.soap.wss","b5.soap.dsig","b5.soap.wsa","b5.soap.attachments","b5.soap.xml","b5.soap.transport","b6.http.wire","b6.http.bodies","b6.http.redirect","b6.http.tls","b6.proxy","b6.auth.apikey","b6.auth.digest","b6.auth.oauth1","b6.auth.oauth2","b6.auth.oidc","b6.auth.sigv4","b6.auth.hawk","b6.runner.record","b6.openapi","b7.auth.gate","b7.auth.wsdl","b7.gql.ws.init"],"name":"restk-testserver","routes":[{"path":"/http/echo","group":"HTTP","purpose":"JSON describing the incoming request","patterns":["/http/echo"]},{"path":"/http/echo-headers","group":"HTTP","purpose":"JSON map of request headers","patterns":["/http/echo-headers"]},{"path":"/http/echo-body","group":"HTTP","purpose":"verbatim copy of request body","patterns":["/http/echo-body"]},{"path":"/http/status/{code}","group":"HTTP","purpose":"returns the given HTTP status","patterns":["/http/status/"]},{"path":"/http/delay?ms=N","group":"HTTP","purpose":"sleeps N ms before responding","patterns":["/http/delay"],"params":["ms"]},{"path":"/http/json/{shape}","group":"HTTP","purpose":"user|list|empty canned JSON","patterns":["/http/json/"]},{"path":"/http/redirect/{n}?status=N","group":"HTTP","purpose":"chain of n redirects (?status=301|302|307|308) ending at /http/echo","patterns":["/http/redirect/"],"params":["status"]},{"path":"/http/cookies","group":"HTTP","purpose":"echo request cookies as JSON","patterns":["/http/cookies"]},{"path":"/http/cookies/set?name=...\u0026value=...","group":"HTTP","purpose":"sets a Set-Cookie response header","patterns":["/http/cookies/set"],"params":["name","value"]},{"path":"/http/gzip","group":"HTTP","purpose":"gzip-compressed JSON response (Content-Encoding: gzip)","patterns":["/http/gzip"]},{"path":"/http/etag","group":"HTTP","purpose":"stable ETag + 304 Not Modified on If-None-Match","patterns":["/http/etag"]},{"path":"/http/large-body?bytes=N","group":"HTTP","purpose":"N-byte JSON body (default 1MB, cap 32MB)","patterns":["/http/large-body"],"params":["bytes"]},{"path":"/http/slow-stream?bytes=N\u0026intervalMs=M","group":"HTTP","purpose":"stream N bytes in 64-byte chunks every M ms","patterns":["/http/slow-stream"],"params":["bytes","intervalMs"]},{"path":"/http/multipart","group":"HTTP","purpose":"echo multipart/form-data parts as JSON","patterns":["/http/multipart"]},{"path":"/http/form-urlencoded","group":"HTTP","purpose":"echo application/x-www-form-urlencoded body as JSON","patterns":["/http/form-urlencoded"]},{"path":"/http/html","group":"HTTP","purpose":"small HTML document (text/html)","patterns":["/http/html"]},{"path":"/http/xml","group":"HTTP","purpose":"small XML document (application/xml; not SOAP)","patterns":["/http/xml"]},{"path":"/http/cache-control?maxAge=N","group":"HTTP","purpose":"emits Cache-Control: public, max-age=N","patterns":["/http/cache-control"],"params":["maxAge"]},{"path":"/http/flaky?fails=N\u0026id=KEY\u0026status=CODE\u0026retryAfter=S\u0026retryAfterDate=1","group":"HTTP","purpose":"fails the first N calls per id (default 500; 429 for rate-limit retry), then 200 {attempts:N+1} and resets — auto-retry proof; retryAfter=S adds Retry-After (seconds, or an HTTP-date with retryAfterDate=1) to the failures (B6-13)","patterns":["/http/flaky"],"params":["fails","id","status","retryAfter","retryAfterDate"]},{"path":"/http/mtls-echo","group":"HTTP","purpose":"{clientCN, clientIssuer, clientCertPresented, message} of the client certificate (meaningful on the mTLS listener :8443; elsewhere clientCertPresented:false, empty CN)","patterns":["/http/mtls-echo"]},{"path":"/http/binary-upload","group":"HTTP","purpose":"accept raw body (any Content-Type); echo size+contentType+hex-preview as JSON (target for Restk's Binary body type)","patterns":["/http/binary-upload"]},{"path":"/http/method/{verb}","group":"HTTP","purpose":"only the named verb succeeds; others → 405 + Allow header","patterns":["/http/method/"]},{"path":"/http/head","group":"HTTP","purpose":"HEAD with Content-Length but no body","patterns":["/http/head"]},{"path":"/http/options","group":"HTTP","purpose":"OPTIONS echoing Allow + CORS headers","patterns":["/http/options"]},{"path":"/http/anything","group":"HTTP","purpose":"echo method/headers/args(query)/form(body)/json/data (httpbin-style)","patterns":["/http/anything","/http/anything/"]},{"path":"/http/require-content-type/{kind}","group":"HTTP","purpose":"415 unless Content-Type matches (json|xml|form|text)","patterns":["/http/require-content-type/"]},{"path":"/http/upload/limit?max=N","group":"HTTP","purpose":"413 if body exceeds N bytes","patterns":["/http/upload/limit"],"params":["max"]},{"path":"/http/headers/one/{name}","group":"HTTP","purpose":"return a single named request header","patterns":["/http/headers/one/"]},{"path":"/http/response-headers?k=v","group":"HTTP","purpose":"reflect query params as response headers","patterns":["/http/response-headers"],"params":["k"]},{"path":"/http/host","group":"HTTP","purpose":"echo the Host header the server saw","patterns":["/http/host"]},{"path":"/http/status/empty/{code}","group":"HTTP","purpose":"given status with an empty body","patterns":["/http/status/empty/"]},{"path":"/http/redirect-to?url=...\u0026status=N","group":"HTTP","purpose":"redirect to a relative or same-host URL (default 302; other hosts → 400)","patterns":["/http/redirect-to"],"params":["url","status"]},{"path":"/http/redirect-code/{code}","group":"HTTP","purpose":"single redirect with the given 3xx code","patterns":["/http/redirect-code/"]},{"path":"/http/redirect-preserve","group":"HTTP","purpose":"307 — method + body preserved","patterns":["/http/redirect-preserve"]},{"path":"/http/redirect-downgrade","group":"HTTP","purpose":"302 — POST downgrades to GET on follow","patterns":["/http/redirect-downgrade"]},{"path":"/http/deflate","group":"HTTP","purpose":"deflate-compressed JSON (Content-Encoding: deflate)","patterns":["/http/deflate"]},{"path":"/http/brotli","group":"HTTP","purpose":"brotli-compressed JSON (Content-Encoding: br)","patterns":["/http/brotli"]},{"path":"/http/cookies/set-attributes?domain=D","group":"HTTP","purpose":"Set-Cookie with Path/Max-Age/HttpOnly/SameSite/Secure; domain=D adds domain_cookie with Domain=D (B6-14)","patterns":["/http/cookies/set-attributes"],"params":["domain"]},{"path":"/http/cookies/delete?name=...","group":"HTTP","purpose":"expire a cookie (Max-Age=0)","patterns":["/http/cookies/delete"],"params":["name"]},{"path":"/http/drip?bytes=N\u0026durationMs=M","group":"HTTP","purpose":"trickle N bytes over M ms (slow-response test); aliases numbytes / duration","patterns":["/http/drip"],"params":["bytes","durationMs"]},{"path":"/http/connection/reset","group":"HTTP","purpose":"hijack + close TCP with no HTTP response (transport error)","patterns":["/http/connection/reset"]},{"path":"/http/bytes/{n}","group":"HTTP","purpose":"n random bytes (application/octet-stream, cap 64MB)","patterns":["/http/bytes/"]},{"path":"/http/stream/{n}","group":"HTTP","purpose":"n newline-delimited JSON lines, chunked","patterns":["/http/stream/"]},{"path":"/http/download/{name}?utf8=1","group":"HTTP","purpose":"Content-Disposition attachment (filename); utf8=1 adds RFC 6266 filename*=UTF-8''… with an ASCII fallback (B6-12)","patterns":["/http/download/"],"params":["utf8"]},{"path":"/http/plain | /http/css | /http/javascript","group":"HTTP","purpose":"text/plain, text/css, application/javascript bodies","patterns":["/http/plain","/http/css","/http/javascript"]},{"path":"/http/content-type-mismatch","group":"HTTP","purpose":"JSON body sent with Content-Type: text/plain","patterns":["/http/content-type-mismatch"]},{"path":"/http/image/{png|jpeg|gif|svg|svg-script}","group":"HTTP","purpose":"a small image of the given type; svg-script = SVG with \u003cscript\u003e + onload that a viewer must NOT run (B6-12)","patterns":["/http/image/"]},{"path":"/shop/login","group":"Shop","purpose":"POST {username:qa, password:shop-pass} → bearer token","patterns":["/shop/login"]},{"path":"/shop/search?q=\u0026category=\u0026maxPrice=\u0026sort=\u0026page=","group":"Shop","purpose":"product search with filters + pagination","patterns":["/shop/search"],"params":["q","category","maxPrice","sort","page"]},{"path":"/shop/categories","group":"Shop","purpose":"list product categories","patterns":["/shop/categories"]},{"path":"/shop/products/{id}","group":"Shop","purpose":"product detail (p-1001..p-1008)","patterns":["/shop/products","/shop/products/"]},{"path":"/shop/cart[/{id}/items[/{itemId}]]","group":"Shop","purpose":"create cart; add/update/remove items (bearer required)","patterns":["/shop/cart","/shop/cart/"]},{"path":"/shop/checkout","group":"Shop","purpose":"POST cartId → order (computes totals)","patterns":["/shop/checkout"]},{"path":"/shop/payment","group":"Shop","purpose":"POST — card ...0000 → 402 declined; paying twice → 409 (idempotency)","patterns":["/shop/payment"]},{"path":"/shop/order/{id}","group":"Shop","purpose":"order status","patterns":["/shop/order/"]},{"path":"/shop/reset","group":"Shop","purpose":"POST — wipe carts/orders back to seed","patterns":["/shop/reset"]},{"path":"/auth/bearer?expected=...","group":"Auth","purpose":"200 if Bearer token matches, 401 otherwise","patterns":["/auth/bearer"],"params":["expected"]},{"path":"/auth/basic?user=...\u0026pass=...","group":"Auth","purpose":"200 if Basic credentials match, 401 otherwise","patterns":["/auth/basic"],"params":["user","pass"]},{"path":"/auth/apikey?in=header|query|cookie\u0026name=...\u0026header=...\u0026expected=...","group":"Auth","purpose":"200 if the key matches, 401 otherwise; in=header (default, name/header = header name, default X-API-Key) | query (param name) | cookie (cookie name) (B6-19)","patterns":["/auth/apikey"],"params":["in","name","header","expected"]},{"path":"/auth/digest?user=\u0026pass=\u0026algorithm=\u0026qop=\u0026stale=1\u0026userhash=1","group":"Auth","purpose":"RFC 7616/2069 Digest challenge/response; algorithm=MD5|MD5-sess|SHA-256|SHA-256-sess (default MD5), qop=auth|auth-int|none (default auth); recomputes + verifies, echoes expected/received on mismatch; B6-20: algorithm SHA-512-256[-sess]; stale=1 → HMAC nonces with a generation (a correct gen-1 response → 401 stale=true + gen-2 nonce, then 200 staleRetried); userhash=1 → RFC 7616 userhash","patterns":["/auth/digest"],"params":["user","pass","algorithm","qop","stale","userhash"]},{"path":"/auth/oauth2/token?ttl=S","group":"Auth","purpose":"POST grant_type=client_credentials|password|refresh_token|authorization_code|urn:ietf:params:oauth:grant-type:device_code → access_token (Basic or form body for client_id/secret; private_key_jwt; tls_client_auth). B6-22: ttl=S → expiring test-token-ttl-… + rotating refresh (reuse → invalid_grant, family revoked); received_params echo; scope openid → RS256 id_token. B6-23: PKCE code exchange verifies code_verifier / redirect_uri / client_id, single use","patterns":["/auth/oauth2/token"],"params":["ttl"]},{"path":"/auth/oauth2/protected","group":"Auth","purpose":"200 if Bearer matches a token issued by /auth/oauth2/token (or an id_token from it); TTL tokens → 401 invalid_token + WWW-Authenticate: Bearer error=\"invalid_token\" after expiry (B6-22)","patterns":["/auth/oauth2/protected"]},{"path":"/oauth/authorize?redirect_uri=...\u0026state=...\u0026response_type=code|token\u0026code_challenge=...\u0026code_challenge_method=S256|plain\u0026tamperState=1\u0026deny=1\u0026ttl=S","group":"Auth","purpose":"no-interaction authorization endpoint: 302 back to redirect_uri with a fixed code + echoed state; B6-23: code_challenge → signed PKCE code (10 min, single use); response_type=token → implicit #access_token= fragment; tamperState=1 → a different state; deny=1 → error=access_denied","patterns":["/oauth/authorize"],"params":["redirect_uri","state","response_type","code_challenge","code_challenge_method","tamperState","deny","ttl"]},{"path":"/auth/jwt-asym","group":"Auth","purpose":"verify a standalone asymmetric (RS*) Bearer JWT against the test client cert public key","patterns":["/auth/jwt-asym"]},{"path":"/auth/awssigv4","group":"Auth","purpose":"AWS Signature v4 (access AKIDEXAMPLE / secret wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY); 200 if signature matches, 403 + canonicalRequest/stringToSign on mismatch; B6-26: presigned URLs (X-Amz-Algorithm/Credential/Date/Expires/SignedHeaders/Signature in the query; expiry enforced), X-Amz-Security-Token (sessionToken), UNSIGNED-PAYLOAD (unsignedPayload)","patterns":["/auth/awssigv4","/auth/awssigv4/"]},{"path":"/auth/oauth1[/plaintext]","group":"Auth","purpose":"OAuth 1.0a (consumer oauth1-key/oauth1-secret, token oauth1-token/oauth1-token-secret); HMAC-SHA1|HMAC-SHA256|PLAINTEXT|RSA-SHA1 (B6-21: /auth/jwt/keys RSA key); 200 or 401 + baseString; B6-21: oauth_* in the Authorization header, the query or the form body (paramsIn)","patterns":["/auth/oauth1","/auth/oauth1/"]},{"path":"/auth/hawk[/sha1]?skewCheck=1","group":"Auth","purpose":"Hawk (id hawk-id / key hawk-key; sha256 default, /sha1 selects sha1); 200 or 401 + normalized string; optional payload-hash check; B6-26: skewCheck=1 → ts more than 60 s off → 401 + WWW-Authenticate: Hawk ts, tsm (re-sync)","patterns":["/auth/hawk","/auth/hawk/"],"params":["skewCheck"]},{"path":"/auth/jwt","group":"Auth","purpose":"verify Authorization: Bearer \u003cjwt\u003e (or ?token=); HS/RS/PS/ES/EdDSA against fixed keys; validates exp/nbf; rejects alg=none; 200 + decoded claims, else 401 + decoded header/claims + reason","patterns":["/auth/jwt"]},{"path":"/auth/jwt/keys","group":"Auth","purpose":"fixed signing material (HMAC secret + RSA/EC/Ed25519 private-key PEMs) so QA can sign tokens /auth/jwt accepts — TEST keys, not secret","patterns":["/auth/jwt/keys"]},{"path":"/script/matchers","group":"Script","purpose":"one JSON object with a value of every JS type + nested/array/empty shapes — exercises EVERY nova.expect matcher (toBe/toEqual/toBeType/toContain/toHaveLength/toHaveProperty/toBeCloseTo/toMatch/toBeOneOf/toBeEmpty/toBeInstanceOf/.not)","patterns":["/script/matchers"]},{"path":"/script/text","group":"Script","purpose":"fixed text/plain body — nova.response.text()/body, contentType, isJSON=false, toContain/toMatch","patterns":["/script/text"]},{"path":"/script/headers","group":"Script","purpose":"fixed X-Request-Id/X-Rate-Limit/X-Custom-Header response headers — nova.response.getHeader(name)/getHeaders()","patterns":["/script/headers"]},{"path":"/script/status/{code}","group":"Script","purpose":"returns {code} with body {code,reason} — nova.response.status/code/reason/isSuccess","patterns":["/script/status/"]},{"path":"/script/delay?ms=N","group":"Script","purpose":"sleeps N ms (default 300, cap 5000) — nova.response.responseTime","patterns":["/script/delay"],"params":["ms"]},{"path":"/script/size?bytes=N","group":"Script","purpose":"N-byte ASCII body (default 512) so nova.response.size == text().length — nova.response.size","patterns":["/script/size"],"params":["bytes"]},{"path":"/soap/fault?status=500","group":"SOAP","purpose":"returns a SOAP 1.1 Fault envelope (HTTP 500; B5-08 status=200–599 picks the HTTP status, e.g. 200 for a non-compliant server)","patterns":["/soap/fault"],"params":["status"]},{"path":"/soap/ok?gzip=1","group":"SOAP","purpose":"returns a SOAP 1.1 success envelope (HTTP 200); B5-18 gzip=1 sends the same bytes Content-Encoding: gzip (+ X-Restk-Content-Encoding: gzip, which survives transparent decoding)","patterns":["/soap/ok"],"params":["gzip"]},{"path":"/soap/wss-verify?user=U\u0026pass=P\u0026replay=reject\u0026reset=1","group":"SOAP","purpose":"validates incoming wsse:UsernameToken; 200 if valid else SOAP Fault. B5-10 replay=reject: a wsse:Nonce is accepted once per session (X-Restk-Session / ?session=) within 5 min (≤ 1000 nonces) — reuse → wsse:FailedAuthentication; no nonce → wsse:InvalidSecurityToken; success adds nonceStatus=fresh; reset=1 first clears the session's nonce cache (repeatable collection runs)","patterns":["/soap/wss-verify"],"params":["user","pass","replay","reset"]},{"path":"/soap/ok12","group":"SOAP","purpose":"SOAP 1.2 success envelope (application/soap+xml)","patterns":["/soap/ok12"]},{"path":"/soap/fault12?status=500","group":"SOAP","purpose":"SOAP 1.2 fault (Code/Value soap:Sender + Reason/Text), HTTP 500; B5-08 status= picks the HTTP status (e.g. 400 for env:Sender)","patterns":["/soap/fault12"],"params":["status"]},{"path":"/soap/fault-codeonly","group":"SOAP","purpose":"1.1 fault with \u003cfaultcode\u003e but no \u003cfaultstring\u003e","patterns":["/soap/fault-codeonly"]},{"path":"/soap/fault-stringonly","group":"SOAP","purpose":"1.1 fault with \u003cfaultstring\u003e but no \u003cfaultcode\u003e","patterns":["/soap/fault-stringonly"]},{"path":"/soap/fault12-reasononly","group":"SOAP","purpose":"1.2 fault with \u003cReason\u003e but no \u003cCode\u003e","patterns":["/soap/fault12-reasononly"]},{"path":"/soap/echo?raw=1\u0026wsdl","group":"SOAP","purpose":"reflects request operation name, body child elements, and SOAPAction back. B5-17 raw=1 adds raw (base64), rawBytes, rawSha256, rawText (CDATA, when valid XML text) of the request bytes (≤ 1 MiB); B5-07 GET ?wsdl serves /wsdl/echo.wsdl","patterns":["/soap/echo"],"params":["raw","wsdl"]},{"path":"/soap/action-required?action=...","group":"SOAP","purpose":"200 only if SOAPAction header matches (default urn:DoThing), else fault","patterns":["/soap/action-required"],"params":["action"]},{"path":"/soap/malformed","group":"SOAP","purpose":"non-XML/truncated body so nova.soap.parse() yields null","patterns":["/soap/malformed"]},{"path":"/soap/large?count=N\u0026base64Bytes=N\u0026bytes=N","group":"SOAP","purpose":"valid envelope with N repeated \u003citem\u003e rows (default 500, cap 5000). B5-15 base64Bytes=N: \u003cdata\u003e = base64 of N deterministic bytes + size + sha256; bytes=N: envelope of EXACTLY N bytes (rows + rows count + pad). Both streamed; public ≤ payload cap (X-Restk-Capped: payload), internal ≤ 50 MiB","patterns":["/soap/large"],"params":["count","base64Bytes","bytes"]},{"path":"/soap/wss-timestamp?maxSkewSec=60\u0026ttlSec=300","group":"SOAP","purpose":"200 only if request carries wsu:Timestamp/Created. B5-10 maxSkewSec / ttlSec (either turns the clock checks on; defaults 60 / 300): Created in the future beyond the skew → wsse:InvalidSecurity, older than ttl+skew or Expires passed → wsse:MessageExpired; success adds expires + checked","patterns":["/soap/wss-timestamp"],"params":["maxSkewSec","ttlSec"]},{"path":"/soap/fault-detail","group":"SOAP","purpose":"1.1 fault with \u003cfaultactor\u003e + \u003cdetail\u003e app-error payload","patterns":["/soap/fault-detail"]},{"path":"/soap/fault12-full?depth=1\u0026langs=en","group":"SOAP","purpose":"1.2 fault with Code/Value + Subcode + Reason + Role + Detail. B5-08 depth=N (1–8) nests N Subcodes (err:BadArgument, err:QuantityTooLarge, err:LimitExceeded, err:LevelN); langs=a,b one Reason/Text per xml:lang (en, de, fr, es, ja; other = English + tag)","patterns":["/soap/fault12-full"],"params":["depth","langs"]},{"path":"/soap/echo-with-header","group":"SOAP","purpose":"reflects request operation + SOAP \u003cHeader\u003e child names back","patterns":["/soap/echo-with-header"]},{"path":"/soap/ok-prefixed","group":"SOAP","purpose":"1.1 success using env: envelope prefix (namespace-agnostic check)","patterns":["/soap/ok-prefixed"]},{"path":"/soap/fault-prefixed","group":"SOAP","purpose":"1.1 fault using env: envelope prefix","patterns":["/soap/fault-prefixed"]},{"path":"/soap/enterprise","group":"SOAP","purpose":"enterprise SOAP oracle (target of /wsdl/enterprise.wsdl): echoes the operation wrapper it saw","patterns":["/soap/enterprise"]},{"path":"/soap/mtls-enterprise","group":"SOAP","purpose":"enterprise SOAP oracle on the mTLS listener (:8443)","patterns":["/soap/mtls-enterprise"]},{"path":"/wsdl/echo.wsdl","group":"WSDL","purpose":"SOAP 1.1 EchoService: Echo (string part) + Ping operations → /soap/echo","patterns":["/wsdl/echo.wsdl"]},{"path":"/wsdl/ping12.wsdl","group":"WSDL","purpose":"SOAP 1.2 PingService → /soap/ok12 (1.2 version detection)","patterns":["/wsdl/ping12.wsdl"]},{"path":"/wsdl/multi.wsdl","group":"WSDL","purpose":"SOAP 1.1 two services (Echo → /soap/echo, User → /soap/ok) — exercises service/port pickers","patterns":["/wsdl/multi.wsdl"]},{"path":"/wsdl/enterprise.wsdl","group":"WSDL","purpose":"enterprise WSDL (SOAP 1.1 + 1.2 + HTTP bindings, xsd:import) → /soap/enterprise","patterns":["/wsdl/enterprise.wsdl"]},{"path":"/wsdl/enterprise-types.xsd","group":"WSDL","purpose":"XSD imported by /wsdl/enterprise.wsdl","patterns":["/wsdl/enterprise-types.xsd"]},{"path":"/ws/echo","group":"WebSocket","purpose":"echo any text/binary frame back","patterns":["/ws/echo"]},{"path":"/ws/echo-once","group":"WebSocket","purpose":"echo first frame then close(1000, 'echoed-once')","patterns":["/ws/echo-once"]},{"path":"/ws/close-with-code/{code}?reason=...","group":"WebSocket","purpose":"accept then close with given code","patterns":["/ws/close-with-code/"],"params":["reason"]},{"path":"/ws/reject-handshake/{status}","group":"WebSocket","purpose":"refuse the WS upgrade with HTTP status; 426 + ?versions=13,8 adds Sec-WebSocket-Version (B3-04)","patterns":["/ws/reject-handshake/"],"params":["versions"]},{"path":"/ws/require-subprotocol?expected=NAME","group":"WebSocket","purpose":"only accept if Sec-WebSocket-Protocol matches","patterns":["/ws/require-subprotocol"],"params":["expected"]},{"path":"/ws/require-auth?bearer=TOKEN","group":"WebSocket","purpose":"401 if Authorization Bearer mismatches","patterns":["/ws/require-auth"],"params":["bearer"]},{"path":"/ws/burst?count=N\u0026intervalMs=M","group":"WebSocket","purpose":"send N text frames at M ms intervals","patterns":["/ws/burst"],"params":["count","intervalMs"]},{"path":"/ws/slow-first?delayMs=N","group":"WebSocket","purpose":"wait N ms then send 'delayed'","patterns":["/ws/slow-first"],"params":["delayMs"]},{"path":"/ws/large-message?bytes=N","group":"WebSocket","purpose":"send one ~N-byte text frame (?binary=1 → binary frame, byte i%256); cap 32 MiB internal / 5 MiB public — \u003e 16 MiB (CLI read limit → 1009) needs the bypass (B3-10)","patterns":["/ws/large-message"],"params":["bytes","binary"]},{"path":"/ws/no-pong","group":"WebSocket","purpose":"ignore client pings (test ping-timeout UX)","patterns":["/ws/no-pong"]},{"path":"/ws/binary-echo","group":"WebSocket","purpose":"echo binary frames; close 1003 on text","patterns":["/ws/binary-echo"]},{"path":"/ws/server-push?intervalMs=N\u0026count=M","group":"WebSocket","purpose":"server pushes M frames at N-ms intervals; no client send required","patterns":["/ws/server-push"],"params":["intervalMs","count"]},{"path":"/ws/ping-from-server?intervalMs=N\u0026count=M","group":"WebSocket","purpose":"server sends WS PING control frames; tests client auto-pong; ?unsolicitedPong=1 sends PONG frames with no ping instead, then {\"pongsSent\":M} (B3-13)","patterns":["/ws/ping-from-server"],"params":["intervalMs","count","unsolicitedPong"]},{"path":"/ws/abrupt-drop?sendFirst=1","group":"WebSocket","purpose":"accepts handshake then closes TCP without a WS close frame → fires onError on the client (vs onClose for /ws/close-with-code/*)","patterns":["/ws/abrupt-drop"],"params":["sendFirst"]},{"path":"/ws/fragmented","group":"WebSocket","purpose":"one text message sent as a first frame + continuation frames — client must reassemble one onMessage; B3-09: splitUtf8=1 cuts inside 東京 code points, binary=1 = binary opcode (parts × bytesPerPart bytes, byte i%256), interleavePing=1 = a ping between fragments, then {pingsSent,pongsReceived}","patterns":["/ws/fragmented"],"params":["parts","bytesPerPart","splitUtf8","binary","interleavePing"]},{"path":"/sse/events?count=N\u0026intervalMs=M","group":"SSE","purpose":"emit N data: events; eol=crlf|cr|mixed line endings, splitEol=1 (with crlf) splits each CRLF across two flushes (data = event-i\\npart-2), truncateLast=1 partial last event + drop, gzip=1 (flush per event) | buffered (one flush at the end)","patterns":["/sse/events"],"params":["count","intervalMs","eol","splitEol","truncateLast","gzip"]},{"path":"/sse/typed-events?types=a,b,c","group":"SSE","purpose":"emit one event per type with id","patterns":["/sse/typed-events"],"params":["types"]},{"path":"/sse/heartbeat?count=N\u0026intervalMs=M","group":"SSE","purpose":"emit only `:` heartbeats","patterns":["/sse/heartbeat"],"params":["count","intervalMs"]},{"path":"/sse/reconnect-with-id?total=N","group":"SSE","purpose":"honors Last-Event-ID for resume","patterns":["/sse/reconnect-with-id"],"params":["total"]},{"path":"/sse/retry-directive?retryMs=N","group":"SSE","purpose":"send `retry: N` directive","patterns":["/sse/retry-directive"],"params":["retryMs"]},{"path":"/sse/require-auth?bearer=TOKEN","group":"SSE","purpose":"401 if Authorization mismatches","patterns":["/sse/require-auth"],"params":["bearer"]},{"path":"/sse/large-event?bytes=N","group":"SSE","purpose":"emit one ~N-byte data: event","patterns":["/sse/large-event"],"params":["bytes"]},{"path":"/sse/multi-line-data","group":"SSE","purpose":"emit event with 3 data: lines","patterns":["/sse/multi-line-data"]},{"path":"/sse/drop-after?count=N","group":"SSE","purpose":"emit N events then forcibly drop socket","patterns":["/sse/drop-after"],"params":["count"]},{"path":"/sse/slow-events?count=N\u0026intervalMs=M","group":"SSE","purpose":"emit N events spaced M ms apart (default 10 / 1000ms — AI-stream-style)","patterns":["/sse/slow-events"],"params":["count","intervalMs"]},{"path":"/sse/cors","group":"SSE","purpose":"emits 3 events with CORS headers (Access-Control-Allow-Origin: *); supports preflight OPTIONS","patterns":["/sse/cors"]},{"path":"/sse/split-utf8","group":"SSE","purpose":"event whose last 4-byte UTF-8 character is split across two flushes — client must buffer the partial rune","patterns":["/sse/split-utf8"]},{"path":"/sse/bom?position=start|middle","group":"SSE","purpose":"stream starts with a UTF-8 BOM — client must strip it so the first event dispatches; position=middle puts the BOM before the 2nd event instead (NOT stripped → that event is ignored; bom-clean + third dispatch)","patterns":["/sse/bom"],"params":["position"]},{"path":"/sse/retry-zero","group":"SSE","purpose":"retry: 0 then drop — OBSERVE: client must clamp backoff, not spin","patterns":["/sse/retry-zero"]},{"path":"/sse/no-newline","group":"SSE","purpose":"a long line with no newline/terminator, then drop — client must cap its buffer (output capped server-side)","patterns":["/sse/no-newline"]},{"path":"/sse/ai-stream?format=openai|anthropic|openai-responses|gemini","group":"SSE","purpose":"LLM-style token stream: OpenAI deltas ending data: [DONE], or Anthropic content_block_delta events ending message_stop. B4-09: requirePost=1 (405 on GET), tools=2 (parallel calls), anthropic tools=1 (input_json_delta) / thinking=1 / ping=1 / error=1 (overloaded_error), format=openai-responses (sequence_number), format=gemini, unknownEvents=1, pauseMs (≤ 30 s)","patterns":["/sse/ai-stream"],"params":["format","tools","finish","error","requirePost","ping","thinking","unknownEvents","pauseMs"]},{"path":"/api/users","group":"REST","purpose":"GET list (?page\u0026limit\u0026status), POST create (201)","patterns":["/api/users"]},{"path":"/api/users/{id}","group":"REST","purpose":"GET (ETag/304), PUT (If-Match/412), DELETE (204)","patterns":["/api/users/"]},{"path":"/api/reset","group":"REST","purpose":"POST — wipe + reseed user store","patterns":["/api/reset"]},{"path":"/graphql","group":"GraphQL","purpose":"stub — use /graphql/shop for schema tests. POST {query,variables,operationName} or GET ?query=...; canned introspection","patterns":["/graphql"]},{"path":"/graphql/always-errors","group":"GraphQL","purpose":"always returns an errors array","patterns":["/graphql/always-errors"]},{"path":"/graphql/partial-errors","group":"GraphQL","purpose":"returns BOTH data and errors (partial-success)","patterns":["/graphql/partial-errors"]},{"path":"/graphql/shop","group":"GraphQL","purpose":"real executable schema (graphql-go): product/products/search/categories queries, addReview mutation, nested reviews, enum, nullable discount, aliases/fragments/variables/directives/introspection, boom (top-level error), flaky (partial error); B2: scalars/interfaces/union/@oneOf/deprecations, echo fields, increment, bulk, connection, slow, uploads (multipart), batching, APQ, application/graphql, gzip, restk_session cookie, ?requireAuth=1|soft, ?csrf=1, ?withExtensions=1, ?schemaVersion=2","patterns":["/graphql/shop"],"params":["session","requireAuth","csrf","withExtensions","schemaVersion","query","variables","operationName","extensions"]},{"path":"/ws/graphql","group":"GraphQL","purpose":"graphql-transport-ws subprotocol: subscribe to {tick}, {ticks(count,intervalMs,from)}, {errorAfter(count)}, {priceUpdates(productId)}, {flakyTicks}, {dropAfter}, {echoExtensions}; query/mutation ops → one next + complete; field errors ride in next.errors; ?strict=1 spec close codes (4400/4401/4408/4409/4429); legacy graphql-ws dialect (start/data/stop/ka/connection_terminate) when that subprotocol is chosen","patterns":["/ws/graphql"],"params":["requireAuth","strict","initTimeoutMs","strictLegacy","kaMs","strictSubprotocol","noEcho","prefer","ackDelayMs","noAck","ackPayload","requireHeaderAuth","serverPingMs","closeAfterMs","closeCode"]},{"path":"/version","group":"Meta","purpose":"this catalog + build (git SHA) + features[] capability tokens — preflight / provenance check","patterns":["/version"]},{"path":"/","group":"Meta","purpose":"this catalog (any unknown path also answers it, HTTP 200)","patterns":["/"]},{"path":"/ws/stomp","group":"STOMP","purpose":"STOMP 1.2 broker (subprotocol v12.stomp); CONNECT/SUBSCRIBE/SEND/UNSUBSCRIBE/DISCONNECT; per-connection echo","patterns":["/ws/stomp"]},{"path":"/ws/mqtt","group":"MQTT","purpose":"MQTT 3.1.1 broker (subprotocol mqtt); CONNECT/SUBSCRIBE/PUBLISH/PINGREQ/DISCONNECT; QoS 0 only, per-connection echo","patterns":["/ws/mqtt"]},{"path":"/auth/oauth2/device","group":"Auth","purpose":"B6-24: RFC 8628 device authorization (POST client_id[, scope, autoApprove=1, ttl]) → {device_code, user_code, verification_uri(_complete), expires_in:600, interval:2}; token polling (grant_type=urn:ietf:params:oauth:grant-type:device_code) → authorization_pending ×2, slow_down ×1, then pending until approved, then one token","patterns":["/auth/oauth2/device"]},{"path":"/auth/oauth2/device/approve?user_code=CODE\u0026deny=1","group":"Auth","purpose":"B6-24: approve (or deny=1) a device user_code","patterns":["/auth/oauth2/device/approve"],"params":["user_code","deny"]},{"path":"/.well-known/openid-configuration","group":"Auth","purpose":"B6-25: OIDC discovery (issuer = this origin; authorize / token / device / jwks endpoints; S256 + plain PKCE)","patterns":["/.well-known/openid-configuration"]},{"path":"/auth/jwt/jwks","group":"Auth","purpose":"B6-25: JWKS with the RS256 test key (kid restk-rsa-1) that signs id_tokens (same key as /auth/jwt/keys RS*)","patterns":["/auth/jwt/jwks"]},{"path":"/ws/connection-count?session=KEY","group":"WebSocket","purpose":"B3-15: first frame = {\"open\":N} live open WebSocket connections of this session (X-Restk-Session or ?session=); any later message re-reads the count; decrements on close","patterns":["/ws/connection-count"],"params":["session"]},{"path":"/contract/valid","group":"Contract Validator","purpose":"body matches the schema → PASS (green badge)","patterns":["/contract/valid"]},{"path":"/contract/valid/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/valid","patterns":["/contract/valid/schema"]},{"path":"/contract/type-mismatch","group":"Contract Validator","purpose":"age is a string, schema wants integer → FAIL (type)","patterns":["/contract/type-mismatch"]},{"path":"/contract/type-mismatch/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/type-mismatch","patterns":["/contract/type-mismatch/schema"]},{"path":"/contract/missing-required","group":"Contract Validator","purpose":"body omits required `name` → FAIL (required)","patterns":["/contract/missing-required"]},{"path":"/contract/missing-required/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/missing-required","patterns":["/contract/missing-required/schema"]},{"path":"/contract/enum","group":"Contract Validator","purpose":"status not in enum → FAIL (enum)","patterns":["/contract/enum"]},{"path":"/contract/enum/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/enum","patterns":["/contract/enum/schema"]},{"path":"/contract/pattern","group":"Contract Validator","purpose":"email fails the regex → FAIL (pattern). (NB: JSON-Schema `format` is annotation-only and not asserted — use `pattern` for a hard fail.)","patterns":["/contract/pattern"]},{"path":"/contract/pattern/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/pattern","patterns":["/contract/pattern/schema"]},{"path":"/contract/nested","group":"Contract Validator","purpose":"nested objects + array of strings → PASS (deep schema)","patterns":["/contract/nested"]},{"path":"/contract/nested/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/nested","patterns":["/contract/nested/schema"]},{"path":"/contract/non-json","group":"Contract Validator","purpose":"XML body → 'contract applies to JSON only' (not applicable, not a failure)","patterns":["/contract/non-json"]},{"path":"/contract/non-json/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/non-json","patterns":["/contract/non-json/schema"]},{"path":"/contract/malicious-ref","group":"Contract Validator","purpose":"SECURITY: pasting this file:// $ref schema must be REJECTED (schema violation), never read off disk","patterns":["/contract/malicious-ref"]},{"path":"/contract/malicious-ref/schema","group":"Contract Validator","purpose":"JSON Schema for /contract/malicious-ref","patterns":["/contract/malicious-ref/schema"]},{"path":"/contract/inferred?drift=none|extra|type|missing","group":"Contract Validator","purpose":"inferred-from-history: run `none` a few times to build the baseline, then a drift variant. type/missing -\u003e WARN (amber); extra (additive) -\u003e PASS by design","patterns":["/contract/inferred"],"params":["drift"]},{"path":"/diff/value?v=1|2","group":"Response Diff","purpose":"value change (credits 120→125)","patterns":["/diff/value"],"params":["v"]},{"path":"/diff/type-change?v=1|2","group":"Response Diff","purpose":"type change (number 120 → string \"120\")","patterns":["/diff/type-change"],"params":["v"]},{"path":"/diff/add-remove?v=1|2","group":"Response Diff","purpose":"field added + field removed","patterns":["/diff/add-remove"],"params":["v"]},{"path":"/diff/key-reorder?v=1|2","group":"Response Diff","purpose":"same object, keys reordered → structural diff shows NO change","patterns":["/diff/key-reorder"],"params":["v"]},{"path":"/diff/array-order?v=1|2","group":"Response Diff","purpose":"array reordered → List sees it, Set ignores it","patterns":["/diff/array-order"],"params":["v"]},{"path":"/diff/array-insert?v=1|2","group":"Response Diff","purpose":"element inserted mid-array","patterns":["/diff/array-insert"],"params":["v"]},{"path":"/diff/nested?v=1|2","group":"Response Diff","purpose":"nested value change (/user/profile/role)","patterns":["/diff/nested"],"params":["v"]},{"path":"/diff/xml?v=1|2","group":"Response Diff","purpose":"XML pair → line diff (Semantic disabled)","patterns":["/diff/xml"],"params":["v"]},{"path":"/diff/html?v=1|2","group":"Response Diff","purpose":"HTML pair → line diff","patterns":["/diff/html"],"params":["v"]},{"path":"/diff/text?v=1|2","group":"Response Diff","purpose":"plain-text pair → line diff","patterns":["/diff/text"],"params":["v"]},{"path":"/diff/mixed?v=1|2","group":"Response Diff","purpose":"v1 JSON, v2 HTML error page → mixed → textual fallback","patterns":["/diff/mixed"],"params":["v"]},{"path":"/diff/binary?v=1|2","group":"Response Diff","purpose":"two PNGs → binary meta-only diff","patterns":["/diff/binary"],"params":["v"]},{"path":"/diff/html-values?v=1|2","group":"Response Diff","purpose":"JSON strings with \u003c, \u003e, \u0026 → stay literal in side-by-side","patterns":["/diff/html-values"],"params":["v"]},{"path":"/diff/large?v=1|2","group":"Response Diff","purpose":"~200 KB body, one element changed → perf headroom","patterns":["/diff/large"],"params":["v"]},{"path":"/graphql/subgraph","group":"GraphQL","purpose":"Apollo Federation v2 subgraph: _service { sdl } (@key), _entities(representations:), product(id)","patterns":["/graphql/subgraph"]},{"path":"/graphql/subgraph/schema.graphql","group":"GraphQL","purpose":"subgraph SDL (same text as _service.sdl)","patterns":["/graphql/subgraph/schema.graphql"]},{"path":"/graphql/incremental?errorInChunk=","group":"GraphQL","purpose":"@defer / @stream as multipart/mixed (Accept incrementalSpec=v0.2 or deferSpec=20220824); no multipart Accept → one complete result; batch → 400","patterns":["/graphql/incremental"],"params":["errorInChunk"]},{"path":"/graphql/login?session=","group":"GraphQL","purpose":"sets the restk_session cookie; /graphql/shop then uses it as the GraphQL session (no ?session=)","patterns":["/graphql/login"],"params":["session"]},{"path":"/graphql/status/{code}","group":"GraphQL","purpose":"answers HTTP {code} (200..599) with a valid GraphQL errors body","patterns":["/graphql/status/"]},{"path":"/graphql/empty-errors","group":"GraphQL","purpose":"executes on the shop schema and answers data + errors: []","patterns":["/graphql/empty-errors"]},{"path":"/graphql/malformed-errors","group":"GraphQL","purpose":"data + error objects without message / with wrong types","patterns":["/graphql/malformed-errors"]},{"path":"/graphql/no-introspection","group":"GraphQL","purpose":"shop schema with introspection disabled (Apollo message, GRAPHQL_VALIDATION_FAILED, 400)","patterns":["/graphql/no-introspection"]},{"path":"/graphql/renamed-roots","group":"GraphQL","purpose":"schema { query: RootQuery, mutation: RootMutation }","patterns":["/graphql/renamed-roots"]},{"path":"/graphql/trusted","group":"GraphQL","purpose":"trusted documents only: POST {documentId:\"sha256:\u003ch\u003e\"}; GET lists the documents; raw query → 400","patterns":["/graphql/trusted"]},{"path":"/graphql/huge?types=\u0026fieldsPerType=","group":"GraphQL","purpose":"generated schema (cached, 16 entries); public ≤ 2000 types × 50 fields, bypass ≤ 10000 × 100","patterns":["/graphql/huge"],"params":["types","fieldsPerType"]},{"path":"/graphql/limited?maxDepth=\u0026maxCost=\u0026rateLimit=","group":"GraphQL","purpose":"shop schema with depth / cost limits (400 + extensions.cost) and a per-session rate limit (429 + Retry-After)","patterns":["/graphql/limited"],"params":["maxDepth","maxCost","rateLimit"]},{"path":"/graphql/gateway-errors?mode=","group":"GraphQL","purpose":"federation gateway error shapes: partial (serviceName reviews) or mode=failed","patterns":["/graphql/gateway-errors"],"params":["mode"]},{"path":"/graphql/shop/schema.graphql","group":"GraphQL","purpose":"SDL of the shop schema (text/plain) — @oneOf, @specifiedBy, interface implements, @deprecated args/input fields, @cacheControl","patterns":["/graphql/shop/schema.graphql"]},{"path":"/graphql/shop/schema.json","group":"GraphQL","purpose":"introspection JSON of the shop schema ({data:{__schema}}, full IntrospectionQuery)","patterns":["/graphql/shop/schema.json"]},{"path":"/graphql/spec","group":"GraphQL","purpose":"strict GraphQL-over-HTTP on the shop schema: Accept negotiation, application/graphql-response+json (400 on parse/validation), 406, 415, 405","patterns":["/graphql/spec"]},{"path":"/graphql/stream","group":"GraphQL","purpose":"GraphQL over SSE (graphql-sse): distinct POST + Accept: text/event-stream → next…complete; single-connection PUT token → GET stream (X-GraphQL-Event-Stream-Token) → POST ops (extensions.operationId) → 202; DELETE stops one","patterns":["/graphql/stream"],"params":["token","operationId","query"]},{"path":"/graphql/multipart-sub?heartbeatMs=","group":"GraphQL","purpose":"Apollo multipart HTTP subscriptions (Accept: multipart/mixed;subscriptionSpec=1.0): {payload} parts + {} heartbeats (default 5 s)","patterns":["/graphql/multipart-sub"],"params":["heartbeatMs"]},{"path":"/ws/graphql/close/{code}?afterMs=","group":"GraphQL","purpose":"acks connection_init, then closes with {code} (1000/1001/1003/1007–1014/3000–4999) after afterMs (default 200)","patterns":["/ws/graphql/close/"],"params":["afterMs"]},{"path":"/http/upload/sink","group":"HTTP","purpose":"B6-07: stream-count the request body → {bytes, sha256}; never buffered. Public ≤ body cap (32 MiB → 413); with the internal bypass ≤ 2 GiB","patterns":["/http/upload/sink"]},{"path":"/http/decompress-request","group":"HTTP","purpose":"B6-08: decode the REQUEST Content-Encoding (gzip | x-gzip | deflate (zlib or raw) | br | zstd | identity) → {contentEncoding, compressedBytes, decodedBytes, decodedSha256, preview}; decoded size \u003e 64 MiB → 413; other encodings → 415","patterns":["/http/decompress-request"]},{"path":"/http/zstd","group":"HTTP","purpose":"B6-09: zstd-compressed JSON (Content-Encoding: zstd)","patterns":["/http/zstd"]},{"path":"/http/encoding/unknown","group":"HTTP","purpose":"B6-09: plain body labelled Content-Encoding: x-foo (client must not pretend to decode it)","patterns":["/http/encoding/unknown"]},{"path":"/http/charset/{name}?lie=1","group":"HTTP","purpose":"B6-10: text in iso-8859-1 | windows-1252 | shift_jis | utf-16le | utf-8-bom | utf-8 bytes with that charset (X-Restk-Expected-Utf8 = pct-encoded expected text); lie=1 labels it with the WRONG charset","patterns":["/http/charset/"],"params":["lie"]},{"path":"/http/json/bignum","group":"HTTP","purpose":"B6-11: exact bytes {\"id\":12345678901234567890,\"big\":…30 digits,\"f\":1e400,\"neg0\":-0.0,\"tiny\":5e-324,\"precise\":0.1000000000000000055511151231257827} (precision / overflow)","patterns":["/http/json/bignum"]},{"path":"/http/json/deep?depth=N","group":"HTTP","purpose":"B6-11: N nested JSON arrays [[[…]]] (default 100, cap 100000), streamed","patterns":["/http/json/deep"],"params":["depth"]},{"path":"/http/pdf","group":"HTTP","purpose":"B6-12: a 1-page PDF (application/pdf)","patterns":["/http/pdf"]},{"path":"/http/msgpack","group":"HTTP","purpose":"B6-12: MessagePack map {id:1,name:\"restk\",ok:true,tags:[\"a\",\"b\"],pi:3.14} (application/msgpack); X-Restk-Decoded-Json = the same value as JSON","patterns":["/http/msgpack"]},{"path":"/http/redirect-loop?status=N","group":"HTTP","purpose":"B6-13: 302 (or N) to itself — the client must stop at its redirect limit","patterns":["/http/redirect-loop"],"params":["status"]},{"path":"/http/cookies/set-raw?header=...","group":"HTTP","purpose":"B6-13: emit each header= value verbatim as a Set-Cookie line (bad dates, odd attributes); CR / LF / NUL or \u003e 4096 bytes → 400","patterns":["/http/cookies/set-raw"],"params":["header"]},{"path":"/http/redirect-cross-host?status=N","group":"HTTP","purpose":"B6-14: 302 (or N) to the SAME path on another host: https://alt.try.restk.ai/http/echo-headers (RESTK_ALT_HOST), or the other loopback name (127.0.0.1 ↔ localhost, same port) on a local run — Authorization must not follow","patterns":["/http/redirect-cross-host"],"params":["status"]},{"path":"/proxy/pac?direct=PATTERN","group":"HTTP","purpose":"B6-18: PAC script: DIRECT for hosts matching the shExpMatch PATTERN (optional; [A-Za-z0-9.*-] only), else PROXY try.restk.ai:8888 (fixed; RESTK_PAC_PROXY)","patterns":["/proxy/pac"],"params":["direct"]},{"path":"/http/raw-target/{anything...}","group":"HTTP","purpose":"B6-01: JSON {rawRequestURI, rawPath, rawQuery, path, method} straight from the request line — no decoding (%2F, ':', trailing slash stay as sent)","patterns":["/http/raw-target/"]},{"path":"/http/raw-headers","group":"HTTP","purpose":"B6-02: hijacked response whose header names keep exact case (x-lower, X-UPPER, X-MiXeD). Direct :8444 only (Caddy re-writes headers)","patterns":["/http/raw-headers"]},{"path":"/http/bad-header?kind=ctl|utf8|obs-fold","group":"HTTP","purpose":"B6-02: hijacked response with an invalid header value: ctl = control byte, utf8 = raw non-ASCII bytes, obs-fold = folded continuation line. Direct :8444 only","patterns":["/http/bad-header"],"params":["kind"]},{"path":"/http/raw-request","group":"HTTP","purpose":"B6-03: echo the raw request header bytes (order + case) as received on the TCP connection. Meaningful on :8444 / :8443 (direct); behind Caddy proxied:true (the bytes are Caddy's)","patterns":["/http/raw-request"]},{"path":"/http/truncated?declared=N\u0026send=M","group":"HTTP","purpose":"B6-04: Content-Length N, then only M \u003c N body bytes, then close (default 100/10). Direct :8444: exactly that. Via Caddy (:443) the client also gets 200 + the declared Content-Length, the M bytes, then the connection closes (the edge does not replace it with an error status)","patterns":["/http/truncated"],"params":["declared","send"]},{"path":"/http/garbage-response","group":"HTTP","purpose":"B6-04: a non-HTTP line, then close. Direct :8444 only (Caddy → 502)","patterns":["/http/garbage-response"]},{"path":"/http/trailers?chunks=N","group":"HTTP","purpose":"B6-05: chunked body (N lines, default 5) + Trailer: X-Checksum = hex SHA-256 of the body","patterns":["/http/trailers"],"params":["chunks"]},{"path":"/http/expect-continue?reject=1|401","group":"HTTP","purpose":"B6-05: with Expect: 100-continue — reject=1 → 417, reject=401 → 401 before the body is read; else read → {expect, bytes, continued}. On the direct :8444 listener the rejection comes WITHOUT a 100 Continue; Caddy (:443) sends 100 Continue itself first, then forwards the 417 / 401","patterns":["/http/expect-continue"],"params":["reject"]},{"path":"/http/protocol","group":"HTTP","purpose":"B6-06: {proto, tlsVersion, alpn, via}. Behind Caddy (via=edge) the values are the CLIENT connection's (X-Restk-Client-* from the trusted edge only); direct listeners report their own","patterns":["/http/protocol"]},{"path":"/http/client-ip","group":"HTTP","purpose":"B6-06: {remoteIP, family: ipv4|ipv6, via} — the client IP after the trusted-edge rule","patterns":["/http/client-ip"]},{"path":"/openapi/{v2|v30|v31}.json","group":"Import","purpose":"B6-28: Swagger 2.0 / OpenAPI 3.0.3 / 3.1.0 spec generated from the route registry (paths, query params, path placeholders, security schemes incl. basic, bearer, API key header/query/cookie, digest, OAuth 2 flows, OpenID Connect); servers = this origin","patterns":["/openapi/"]},{"path":"/runner/record?run=ID","group":"Runner","purpose":"B6-27: POST/PUT/PATCH stores the body + server timestamp for run ID (per X-Restk-Session); GET → {run, count, dropped, records[{seq, receivedAtMs, method, contentType, bytes, sha256, body, truncated}]} in arrival order; DELETE clears. ≤ 1000 records × 64 KiB per run","patterns":["/runner/record"],"params":["run"]},{"path":"/soap/require-version/{1.1|1.2}","group":"SOAP","purpose":"B5-01: POST only. 1.1 needs Content-Type text/xml, 1.2 application/soap+xml — else 415 + a fault in the endpoint's version; envelope namespace ≠ the version → SOAP 1.1 VersionMismatch fault (HTTP 500) with an env:Upgrade/SupportedEnvelope header; match → {version, operation}","patterns":["/soap/require-version/"]},{"path":"/soap/rpc","group":"SOAP","purpose":"B5-03: rpc/literal + rpc/encoded reflector (target of /wsdl/rpc-*.wsdl, bare/any/unqualified.wsdl): \u003cOp\u003eResponse in the op namespace with return (Add = a+b, SumArray = Σ items, Echo = first part) + reflect {operation, operationNamespace, style rpc-literal|rpc-encoded, encodingStyle, partCount, parts (name, namespace, xsiType, arrayType, childCount, text), arrayType, itemCount}","patterns":["/soap/rpc"]},{"path":"/soap/must-understand?understood=A,B","group":"SOAP","purpose":"B5-09: header blocks with mustUnderstand=1|true targeted at this node (no role/actor, next, ultimateReceiver) that are not in `understood` (local names) → MustUnderstand fault (1.1 or 1.2, HTTP 500) + one NotUnderstood header block each (SOAP 1.2 NS, qname); else success listing processed / ignored blocks","patterns":["/soap/must-understand"],"params":["understood"]},{"path":"/soap/wsa-required?version=2005|2004","group":"SOAP","purpose":"B5-13: wsa:Action, wsa:To, wsa:MessageID required in the WS-Addressing 2005/08 (default) or 2004/08 namespace → else fault wsa:MessageAddressingHeaderRequired (2004: MessageInformationHeaderRequired) + wsa:FaultDetail/ProblemHeaderQName; headers in the other namespace → 'namespace mismatch'. Success: wsa:Action \u003caction\u003eResponse, wsa:RelatesTo = MessageID, wsa:To = ReplyTo/Address or anonymous","patterns":["/soap/wsa-required"],"params":["version"]},{"path":"/soap/mtom-echo","group":"SOAP","purpose":"B5-14: multipart/related; type=\"application/xop+xml\" (else 415 / fault): root part parsed (≤ 1 MiB), every xop:Include href=cid: must match a part Content-ID; each attachment streamed into SHA-256 → {mode, rootContentType, operation, includeCount, attachmentCount, attachments[contentId, contentType, transferEncoding, size, sha256, referenced], allReferencesResolved}","patterns":["/soap/mtom-echo"]},{"path":"/soap/swa-echo","group":"SOAP","purpose":"B5-14: SOAP with Attachments — multipart/related; type=\"text/xml\": href=\"cid:…\" attributes in the root envelope resolved against part Content-IDs; same echo as /soap/mtom-echo (mode swa)","patterns":["/soap/swa-echo"]},{"path":"/soap/mtom-response?bytes=N","group":"SOAP","purpose":"B5-14: MTOM response — multipart/related; type=\"application/xop+xml\"; start=\"\u003croot.message@restk\u003e\": root envelope with xop:Include href=\"cid:file1@restk\" + size + sha256, then N (default 1024) deterministic binary bytes. Public: N ≤ payload cap; internal ≤ 50 MiB","patterns":["/soap/mtom-response"],"params":["bytes"]},{"path":"/soap/ok-default-ns","group":"SOAP","purpose":"B5-16: SOAP 1.1 success with the envelope in the DEFAULT namespace (no prefixes anywhere): \u003cEnvelope xmlns=…\u003e\u003cBody\u003e\u003cGetUserResponse xmlns=\"http://example.com/svc\"\u003e…","patterns":["/soap/ok-default-ns"]},{"path":"/soap/cdata","group":"SOAP","purpose":"B5-16: CDATA in the body — Payload = `\u003cb\u003ebold\u003c/b\u003e \u0026 \"quoted\" \u003cnot-a-tag/\u003e`, Mixed = pre-\u003cx/\u003e-post (text + CDATA), Split = a]]\u003eb (two adjacent CDATA sections)","patterns":["/soap/cdata"]},{"path":"/soap/xxe?token=T","group":"SOAP","purpose":"B5-16: response with a DOCTYPE declaring external entities (file:///etc/passwd, an https canary URL, a parameter entity %pe; also pointing at the canary) used in the body; a safe client leaves \u0026xxe; / \u0026remote; unresolved. Marker XXE-SAFE-MARKER","patterns":["/soap/xxe"],"params":["token"]},{"path":"/soap/xxe-canary?token=T\u0026kind=K\u0026check=1","group":"SOAP","purpose":"B5-16: the URL /soap/xxe's external entities point at — each fetch is counted per token (a client that resolved an entity); check=1 → JSON {token, hits{kind:n}, total} without counting","patterns":["/soap/xxe-canary"],"params":["token","kind","check"]},{"path":"/soap/charset?cs=utf-8|iso-8859-1|utf-16|utf-16le|utf-16be\u0026decl=LABEL|none\u0026hdr=LABEL|none\u0026bom=1","group":"SOAP","purpose":"B5-17: success envelope encoded in cs (text 'Grüße, café, ñ, ¿'; UTF variants add wide '☕ 日本'; textUtf8Hex = expected UTF-8 hex). decl = XML declaration encoding (default cs; none = no declaration), hdr = Content-Type charset (default cs; none = no charset), bom=1 prepends the BOM (utf-16 always BE + BOM)","patterns":["/soap/charset"],"params":["cs","decl","hdr","bom"]},{"path":"/soap/redirect?status=307\u0026url=/soap/echo","group":"SOAP","purpose":"B5-18: 301/302/303/307/308 (default 307; other → 307) to url (default /soap/echo); url = relative, or https:// on this host or try.restk.ai; else 400. 307/308 keep POST, body and SOAPAction","patterns":["/soap/redirect"],"params":["status","url"]},{"path":"/soap/wss-signature-verify?requireTimestamp=1\u0026maxSkewSec=60","group":"SOAP","purpose":"B5-11: verifies a WS-Security X.509 signature: wsse:BinarySecurityToken (X509v3) chained to the QA mTLS CA, ds:Signature (exc-c14n, rsa-sha1/256/512, sha1/256/512 digests) covering the Body and the wsu:Timestamp (if present). Faults: wsse:InvalidSecurity, InvalidSecurityToken, FailedCheck (digest / signature), UnsupportedAlgorithm, MessageExpired (requireTimestamp=1). Success: verified, subject, issuer, signatureMethod, reference×n, created, expires","patterns":["/soap/wss-signature-verify"],"params":["requireTimestamp","maxSkewSec"]},{"path":"/sse/parser-edge?case=NAME","group":"SSE","purpose":"B4-01: fixed WHATWG parser byte script per case (empty-data, event-names, event-only, id-nul, id-only, id-reset, multi-data, no-colon, retry-nan, two-spaces, unknown-field); starts with `event: begin` (data = case) and ends with `event: sentinel` whose data JSON states the expected dispatched events + lastEventId","patterns":["/sse/parser-edge"],"params":["case"]},{"path":"/sse/invalid-utf8","group":"SSE","purpose":"B4-03: data: bad-\u003cFF FE\u003e-end (invalid UTF-8 → client shows 2× U+FFFD, no crash), then data: after-invalid","patterns":["/sse/invalid-utf8"]},{"path":"/sse/content-type?ct=TYPE","group":"SSE","purpose":"B4-03: 3 valid events (2nd = 'café ☕' in UTF-8) under Content-Type = ct verbatim; ct empty/absent = NO Content-Type header (client must fail). The header-absent case is reliable only on the direct :8444 listener (https://try.restk.ai:8444, raw wire): behind Caddy (:443) Go's net/http in the proxy may sniff and add 'text/plain; charset=utf-8' when the first event reaches it before the header flush — Caddy has no directive to stop that","patterns":["/sse/content-type"],"params":["ct"]},{"path":"/sse/status/{code}?retryAfter=N","group":"SSE","purpose":"B4-04: answer the SSE connect with {code} (204 empty; 4xx/5xx JSON; 200 = 3 events); retryAfter sets Retry-After; 1xx/3xx → 400","patterns":["/sse/status/"],"params":["retryAfter"]},{"path":"/sse/redirect?url=PATH\u0026status=307","group":"SSE","purpose":"B4-05: 301/302/307/308 to url (default /sse/events?count=3); url = relative, or https:// on this host or try.restk.ai (any port); else 400. Also on the :8443 mTLS listener","patterns":["/sse/redirect"],"params":["url","status"]},{"path":"/sse/infinite?intervalMs=M\u0026ttlSec=S\u0026bytes=B","group":"SSE","purpose":"B4-06: endless ticks (id: N, data JSON {seq,elapsedMs[,pad]}) every M ms (default 1000, min 10) until ttlSec (default 60), then event: end {reason: ttl|stream-cap|message-cap}. Public: ≤ 60 s; internal bypass: ≤ 4 h","patterns":["/sse/infinite"],"params":["intervalMs","ttlSec","bytes"]},{"path":"/sse/echo-last-event-id?resume=1\u0026id=ID","group":"SSE","purpose":"B4-07: event: last-event-id, data JSON {present, raw (non-printable bytes as \\xHH), hex, bytes}. resume=1: without the header send id: ID (default evt-α-1) + data: first-connect, then DROP; with it echo + data: resumed","patterns":["/sse/echo-last-event-id"],"params":["resume","id"]},{"path":"/sse/connection-count?session=KEY\u0026holdMs=N\u0026intervalMs=M","group":"SSE","purpose":"B4-07: data {\"open\":N} = live open SSE streams of this session (X-Restk-Session or ?session=; separate from the WebSocket counter), re-sent every intervalMs (default 250) for holdMs (default 1500, max 30000), then end","patterns":["/sse/connection-count"],"params":["session","holdMs","intervalMs"]},{"path":"/sse/echo-request","group":"SSE","purpose":"B4-08: any method; event: request data JSON {method, path, query, headers, bodyBytes, bodySha256, bodyText (≤ 4 KiB UTF-8)}, then data: echo-1..echo-3. Header values are shown, never logged","patterns":["/sse/echo-request"]},{"path":"/sse/buffered?batch=N\u0026count=M\u0026intervalMs=I","group":"SSE","purpose":"B4-10: simulated proxy buffer — NO X-Accel-Buffering header (X-Restk-Batch: N); events (id: i, data JSON {i,batch}) written every I ms (default 200) but flushed only every N (default 3) events","patterns":["/sse/buffered"],"params":["batch","count","intervalMs"]},{"path":"/sse/server-close?mode=clean|drop|mid-event|headers-only\u0026count=N","group":"SSE","purpose":"B4: server close variants — clean = N events then a graceful end; drop = N events then TCP drop (no terminating chunk); mid-event = N events + an unterminated data: line then drop; headers-only = 200 + headers, no events, graceful end","patterns":["/sse/server-close"],"params":["mode","count"]},{"path":"/ws/deflate?params=...\u0026fragment=1","group":"WebSocket","purpose":"B3-16: permessage-deflate echo. First frame = JSON {offered, negotiated, extensions, params, fragment}; compressed when the client offered permessage-deflate. params = ';'-list (a raw ';', %3B or ',' all work) of server_max_window_bits=8..15, client_max_window_bits=8..15 (answered only when offered), *_no_context_takeover; fragment=1 adds one compressed message split over ≥ 3 frames (RSV1 on the first only)","patterns":["/ws/deflate"],"params":["params","fragment"]},{"path":"/ws/invalid-utf8","group":"WebSocket","purpose":"B3-08: one text frame with the invalid UTF-8 bytes C3 28; a compliant client closes 1007 (echoed); after 5 s without a close the server closes 1000 'invalid-utf8-not-rejected'","patterns":["/ws/invalid-utf8"]},{"path":"/ws/greet-immediately","group":"WebSocket","purpose":"B3-08: the text frame 'greet-at-t0' rides in the SAME TCP write as the 101 (t=0) — the client must not lose it; close 1000 after 500 ms","patterns":["/ws/greet-immediately"]},{"path":"/ws/protocol-violation?kind=rsv1|opcode|masked|bigcontrol","group":"WebSocket","purpose":"B3-11: one bad frame (RSV1 without extension / reserved opcode 3 / masked server frame / 126-byte ping), then wait ≤ 5 s for the client close (1002, echoed) and drop","patterns":["/ws/protocol-violation"],"params":["kind"]},{"path":"/ws/encoded-binary?enc=gzip|msgpack","group":"WebSocket","purpose":"B3-12: binary frame = gzip (1f8b…) or fixed MsgPack bytes of {\"id\":1,\"name\":\"restk\",\"tags\":[\"ws\",\"binary\"],\"ok\":true}, then that JSON as a text frame, then close 1000","patterns":["/ws/encoded-binary"],"params":["enc"]},{"path":"/ws/ignore-close?holdMs=N","group":"WebSocket","purpose":"B3-14: never answers the client close frame; drops TCP holdMs (default/max 30000) after it (close-handshake timeout test)","patterns":["/ws/ignore-close"],"params":["holdMs"]},{"path":"/ws/echo-handshake","group":"WebSocket","purpose":"B3-01: first frame = JSON {headers, cookies, origin, subprotocols, subprotocol, extensions, query, host, path} of the upgrade request (the first offered subprotocol is chosen), then echo; header values are shown, never logged","patterns":["/ws/echo-handshake"]},{"path":"/ws/require-origin?allow=ORIGIN","group":"WebSocket","purpose":"B3-02: 403 unless the Origin header equals allow (default https://try.restk.ai); else 'origin-ok:\u003corigin\u003e' then close 1000","patterns":["/ws/require-origin"],"params":["allow"]},{"path":"/ws/subprotocol-mismatch?mode=unoffered|omit","group":"WebSocket","purpose":"B3-03: unoffered = answer a subprotocol the client did not offer (client must fail); omit = accept without Sec-WebSocket-Protocol; then 'subprotocol-mode:\u003cmode\u003e' + close 1000","patterns":["/ws/subprotocol-mismatch"],"params":["mode"]},{"path":"/ws/redirect?url=PATH\u0026status=307","group":"WebSocket","purpose":"B3-05: 301/302/307/308 before the upgrade (default 307 → /ws/echo); url = relative, or https:// / wss:// on this host or try.restk.ai (any port); else 400","patterns":["/ws/redirect"],"params":["url","status"]},{"path":"/ws/handshake-delay?ms=N","group":"WebSocket","purpose":"B3-06: hold the upgrade request N ms (default 3000, max 30000) before the 101; then 'handshake-delayed:\u003cN\u003e' + close 1000","patterns":["/ws/handshake-delay"],"params":["ms"]},{"path":"/ws/bad-accept","group":"WebSocket","purpose":"B3-07: hand-written 101 with a WRONG Sec-WebSocket-Accept — the client must fail the handshake","patterns":["/ws/bad-accept"]},{"path":"/wsdl/include.wsdl","group":"WSDL","purpose":"B5-02 (P-15): schema with a relative xsd:include of include-part.xsd (same targetNamespace http://restk.test/include); IncludedEcho → /soap/echo","patterns":["/wsdl/include.wsdl"]},{"path":"/wsdl/include-part.xsd","group":"WSDL","purpose":"B5-02: the included schema part (complexType IncludedItem, targetNamespace http://restk.test/include)","patterns":["/wsdl/include-part.xsd"]},{"path":"/wsdl/split/service.wsdl","group":"WSDL","purpose":"B5-02 (P-16): concrete binding + service; wsdl:import location=\"abstract.wsdl\" brings types, messages and the portType; SplitEcho → /soap/echo","patterns":["/wsdl/split/service.wsdl"]},{"path":"/wsdl/split/abstract.wsdl","group":"WSDL","purpose":"B5-02: the abstract half (types, messages, portType SplitPortType) imported by split/service.wsdl","patterns":["/wsdl/split/abstract.wsdl"]},{"path":"/wsdl/nested/a/root.wsdl","group":"WSDL","purpose":"B5-02 (P-17): xsd:import schemaLocation=\"../b/types.xsd\", which imports common/leaf.xsd (relative to b/) — a 3-document parent-relative chain; NestedOrder → /soap/echo","patterns":["/wsdl/nested/a/root.wsdl"]},{"path":"/wsdl/nested/b/types.xsd","group":"WSDL","purpose":"B5-02: Order type; imports common/leaf.xsd","patterns":["/wsdl/nested/b/types.xsd"]},{"path":"/wsdl/nested/b/common/leaf.xsd","group":"WSDL","purpose":"B5-02: Money type (end of the nested chain)","patterns":["/wsdl/nested/b/common/leaf.xsd"]},{"path":"/wsdl/abs-import.wsdl","group":"WSDL","purpose":"B5-02 (P-18): xsd:import with an ABSOLUTE schemaLocation carrying an explicit port (https://try.restk.ai:443/wsdl/enterprise-types.xsd behind the edge; the real port on direct listeners); AbsOrder → /soap/echo","patterns":["/wsdl/abs-import.wsdl"]},{"path":"/wsdl/cycle.wsdl","group":"WSDL","purpose":"B5-02 (P-19): imports cycle-a.xsd, which imports cycle-b.xsd, which imports cycle-a.xsd — an importer must terminate; CycleRoot → /soap/echo","patterns":["/wsdl/cycle.wsdl"]},{"path":"/wsdl/cycle-a.xsd","group":"WSDL","purpose":"B5-02: NodeA (imports cycle-b.xsd)","patterns":["/wsdl/cycle-a.xsd"]},{"path":"/wsdl/cycle-b.xsd","group":"WSDL","purpose":"B5-02: NodeB (imports cycle-a.xsd)","patterns":["/wsdl/cycle-b.xsd"]},{"path":"/wsdl/missing-import.wsdl","group":"WSDL","purpose":"B5-02: xsd:import schemaLocation=\"missing/types.xsd\" → 404 (an importer must report the missing document)","patterns":["/wsdl/missing-import.wsdl"]},{"path":"/wsdl/missing/{any}","group":"WSDL","purpose":"B5-02: always 404 text/plain — the deliberately missing import target","patterns":["/wsdl/missing/"]},{"path":"/wsdl/rpc-literal.wsdl","group":"WSDL","purpose":"B5-03 (P-20): style=\"rpc\" use=\"literal\" namespace=\"urn:restk:rpc\" — Add(a:int, b:int) → return; → /soap/rpc","patterns":["/wsdl/rpc-literal.wsdl"]},{"path":"/wsdl/rpc-encoded.wsdl","group":"WSDL","purpose":"B5-03 (P-21): style=\"rpc\" use=\"encoded\" (SOAP encoding) — Add, SumArray(label:string, values:ArrayOfInt = soapenc:Array of xsd:int); → /soap/rpc","patterns":["/wsdl/rpc-encoded.wsdl"]},{"path":"/wsdl/bare.wsdl","group":"WSDL","purpose":"B5-04 (P-22): document/literal BARE — the message part is the simple element tns:Symbol (no operation wrapper); GetQuote → /soap/rpc","patterns":["/wsdl/bare.wsdl"]},{"path":"/wsdl/any-types.wsdl","group":"WSDL","purpose":"B5-04 (P-24): xsd:anyType, xsd:anySimpleType, xsd:any (##other, lax, unbounded), xsd:anyAttribute; Store → /soap/rpc","patterns":["/wsdl/any-types.wsdl"]},{"path":"/wsdl/unqualified.wsdl","group":"WSDL","purpose":"B5-04 (P-26): elementFormDefault=\"unqualified\" — only the global element is namespace-qualified, children are not; Register → /soap/rpc (reflect shows each child's namespace)","patterns":["/wsdl/unqualified.wsdl"]},{"path":"/wsdl/huge.wsdl?ops=N","group":"WSDL","purpose":"B5-05 (P-27): generated SOAP 1.1 WSDL with N operations (default 100, cap 5000; internal bypass cap 50000), streamed; body ≤ payload cap (fewer ops + X-Restk-Capped: payload). X-Restk-Ops = operations served","patterns":["/wsdl/huge.wsdl"],"params":["ops"]},{"path":"/wsdl/redirect?url=/wsdl/echo.wsdl\u0026status=302","group":"WSDL","purpose":"B5-07 (P-32): 301/302/303/307/308 (default 302) to url (default /wsdl/echo.wsdl); url = relative, or https:// on this host or try.restk.ai; else 400","patterns":["/wsdl/redirect"],"params":["url","status"]},{"path":"/wsdl/malformed.wsdl","group":"WSDL","purpose":"B5-07 (P-34): 200 text/xml that is NOT well-formed (mismatched + unclosed tags) — the importer must fail with a clear parse error","patterns":["/wsdl/malformed.wsdl"]},{"path":"/wsdl/wsdl20.wsdl","group":"WSDL","purpose":"B5-07 (P-35): a valid WSDL 2.0 \u003cdescription\u003e (interface / binding wsoap:version=1.2 / endpoint → /soap/ok12) — a WSDL 1.1-only importer must say 'unsupported'","patterns":["/wsdl/wsdl20.wsdl"]}],"version":"0.1.0"}
