@adonix.org/cloud-spark 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Represents a warning event emitted by a WebSocket.
3
+ */
4
+ type WarnEvent = {
5
+ type: "warn";
6
+ message: string;
7
+ };
8
+ /**
9
+ * Map of custom WebSocket events.
10
+ * - `warn`: internal warning notifications
11
+ * - `open`: triggered when the WebSocket is accepted
12
+ */
13
+ type CustomEventMap = {
14
+ warn: WarnEvent;
15
+ open: Event;
16
+ };
17
+ /** Options for registering WebSocket event listeners. */
18
+ type EventOptions = {
19
+ once?: boolean;
20
+ };
21
+ /** Map of all events, combining native WebSocket events and custom events. */
22
+ type ExtendedEventMap = WebSocketEventMap & CustomEventMap;
23
+ /** Names of all events, including standard and custom WebSocket events. */
24
+ type ExtendedEventType = keyof ExtendedEventMap;
25
+ /** Event listener type for an extended WebSocket event. */
26
+ type ExtendedEventListener<K extends ExtendedEventType> = (ev: ExtendedEventMap[K]) => void;
27
+ /**
28
+ * Represents a user-defined attachment object that can be associated with a WebSocket connection.
29
+ */
30
+ type WSAttachment = object;
31
+ /**
32
+ * Represents a managed WebSocket connection with typed attachment and extended event support.
33
+ *
34
+ * @template A - Type of the attachment object associated with this connection.
35
+ */
36
+ interface WebSocketConnection<A extends WSAttachment> {
37
+ /**
38
+ * Current readyState of the WebSocket (0 = CONNECTING, 1 = OPEN, 2 = CLOSING, 3 = CLOSED).
39
+ */
40
+ get readyState(): number;
41
+ /**
42
+ * Checks whether the WebSocket is currently in one of the provided states.
43
+ *
44
+ * @param states - List of WebSocket readyState values to check against.
45
+ * @returns `true` if the WebSocket's readyState matches any of the provided states.
46
+ */
47
+ isState(...states: number[]): boolean;
48
+ /**
49
+ * Accepts the WebSocket connection if not already accepted.
50
+ *
51
+ * @returns The readonly native WebSocket instance.
52
+ */
53
+ accept(): Readonly<WebSocket>;
54
+ /**
55
+ * Accepts the WebSocket connection in the context of a Durable Object.
56
+ * Optionally associates tags for filtering.
57
+ *
58
+ * @param ctx - DurableObjectState for the WebSocket.
59
+ * @param tags - Optional list of string tags.
60
+ * @returns The readonly native WebSocket instance.
61
+ */
62
+ acceptWebSocket(ctx: DurableObjectState, tags?: string[]): Readonly<WebSocket>;
63
+ /**
64
+ * Retrieves the user-defined attachment object associated with this connection.
65
+ *
66
+ * The returned object is a read-only view of the attachment to prevent
67
+ * accidental mutation. To modify the attachment, call {@link attach}.
68
+ *
69
+ * @returns A read-only view of the current attachment.
70
+ */
71
+ get attachment(): Readonly<A>;
72
+ /**
73
+ * Attaches or updates a user-defined object on this connection.
74
+ *
75
+ * Passing a partial object merges the new properties into the existing
76
+ * attachment, leaving other fields unchanged. Pass `null` to clear
77
+ * the attachment entirely.
78
+ *
79
+ * @param attachment - Partial object containing metadata to attach or update,
80
+ * or `null` to clear the attachment.
81
+ */
82
+ attach(attachment?: Partial<A> | null): void;
83
+ /**
84
+ * Sends a message to the connected WebSocket client.
85
+ *
86
+ * @param message - Message to send, either string or binary data.
87
+ */
88
+ send(message: string | ArrayBuffer): void;
89
+ /**
90
+ * Closes the WebSocket connection with an optional code and reason.
91
+ *
92
+ * @param code - Close code (default is `1000` NORMAL).
93
+ * @param reason - Optional reason string (sanitized to valid characters).
94
+ */
95
+ close(code?: number, reason?: string): void;
96
+ /**
97
+ * Registers an event listener for a WebSocket event.
98
+ *
99
+ * Supports both standard WebSocket events (`message`, `close`, etc.)
100
+ * and custom events (`open`, `warn`).
101
+ *
102
+ * @param type - Event type to listen for.
103
+ * @param listener - Callback invoked when the event occurs.
104
+ * @param options - Optional event listener options (`once`).
105
+ */
106
+ addEventListener<K extends ExtendedEventType>(type: K, listener: ExtendedEventListener<K>, options?: EventOptions): void;
107
+ /**
108
+ * Removes a previously registered WebSocket event listener.
109
+ *
110
+ * Works for both standard and custom events.
111
+ *
112
+ * @param type - Event type to remove.
113
+ * @param listener - Listener function to remove.
114
+ */
115
+ removeEventListener<K extends ExtendedEventType>(type: K, listener: ExtendedEventListener<K>): void;
116
+ }
117
+
118
+ /**
119
+ * Manages active WebSocket connections in a Cloudflare Workers environment.
120
+ *
121
+ * Provides a simple interface for creating, restoring, and managing
122
+ * WebSocket connections with optional attachments. Users can:
123
+ *
124
+ * - Create new WebSocket connections (`create`) and attach arbitrary data.
125
+ * - Accept connections using the standard WebSocket API (`accept`).
126
+ * - Accept connections using the hibernatable WebSocket API (`acceptWebSocket`),
127
+ * which allows the connection to be put to sleep when inactive.
128
+ * - Restore existing WebSockets into a managed session (`restore`, `restoreAll`),
129
+ * maintaining their hibernation state.
130
+ * - Iterate over active connections or retrieve a connection by its WebSocket instance.
131
+ * - Close a connection cleanly with optional code and reason (`close`).
132
+ *
133
+ * @template A - Type of attachment data stored on each WebSocket connection.
134
+ */
135
+ declare class WebSocketSessions<A extends WSAttachment = WSAttachment> {
136
+ /** @internal Map of active WebSocket to their connection wrapper. */
137
+ private readonly map;
138
+ /**
139
+ * Create a new WebSocket connection and optionally attach user data.
140
+ *
141
+ * @param attachment - Partial attachment object to initialize the connection with.
142
+ * @returns A `WebSocketConnection` instance ready for accepting and sending messages.
143
+ */
144
+ create(attachment?: Partial<A>): WebSocketConnection<A>;
145
+ /**
146
+ * Wraps an existing WebSocket in a managed connection session.
147
+ *
148
+ * @param ws - An existing WebSocket to restore.
149
+ * @returns A `WebSocketConnection` representing the restored session.
150
+ */
151
+ restore(ws: WebSocket): WebSocketConnection<A>;
152
+ /**
153
+ * Restores multiple WebSockets into managed sessions at once.
154
+ *
155
+ * @param all - Array of WebSocket instances to restore.
156
+ * @returns Array of `WebSocketConnections` restored.
157
+ */
158
+ restoreAll(all: WebSocket[]): ReadonlyArray<WebSocketConnection<A>>;
159
+ /**
160
+ * Retrieves the managed connection for a specific WebSocket, if any.
161
+ *
162
+ * @param ws - WebSocket instance.
163
+ * @returns Corresponding `WebSocketConnection` or `undefined` if not managed.
164
+ */
165
+ get(ws: WebSocket): WebSocketConnection<A> | undefined;
166
+ /**
167
+ * Selects the managed `WebSocketConnection` objects corresponding to the given WebSockets.
168
+ *
169
+ * @param sockets - Array of WebSocket instances to resolve.
170
+ * @returns Array of corresponding `WebSocketConnection` objects.
171
+ */
172
+ select(sockets: WebSocket[]): WebSocketConnection<A>[];
173
+ /**
174
+ * Returns an iterator over all active `WebSocketConnection` objects
175
+ * managed by this session.
176
+ *
177
+ * Useful for iterating over all connections to perform actions such as
178
+ * broadcasting messages.
179
+ *
180
+ * @returns Iterable iterator of all active `WebSocketConnection` objects.
181
+ */
182
+ values(): IterableIterator<WebSocketConnection<A>>;
183
+ /**
184
+ * Returns an iterator over all active raw `WebSocket` instances
185
+ * currently tracked by this session.
186
+ *
187
+ * @returns Iterable iterator of all active `WebSocket` instances.
188
+ */
189
+ keys(): IterableIterator<WebSocket>;
190
+ /**
191
+ * Closes a managed WebSocket connection with optional code and reason.
192
+ *
193
+ * @param ws - WebSocket to close.
194
+ * @param code - Optional WebSocket close code.
195
+ * @param reason - Optional reason string.
196
+ * @returns `true` if the connection was managed and removed, `false` otherwise.
197
+ */
198
+ close(ws: WebSocket, code?: number, reason?: string): boolean;
199
+ /** Iterates over all active WebSocket connections. */
200
+ [Symbol.iterator](): IterableIterator<WebSocketConnection<A>>;
201
+ /** Registers a connection internally. */
202
+ private register;
203
+ /** Un-registers a connection internally. */
204
+ private unregister;
205
+ }
206
+
207
+ export { WebSocketSessions };
@@ -0,0 +1,29 @@
1
+ import { b as Middleware } from './middleware-CfKw8AlN.js';
2
+
3
+ /**
4
+ * Returns a middleware that validates incoming WebSocket upgrade requests.
5
+ *
6
+ * - Only validates the upgrade request; it does **not** perform the actual WebSocket upgrade.
7
+ * - Ensures the request:
8
+ * - Uses the `GET` method.
9
+ * - Matches the specified path, supporting `path-to-regexp` style patterns
10
+ * (e.g., `/chat/:name`).
11
+ * - Contains required WebSocket headers:
12
+ * - `Connection: Upgrade`
13
+ * - `Upgrade: websocket`
14
+ * - `Sec-WebSocket-Version: 13`
15
+ * - Returns an error response if validation fails, otherwise passes control to
16
+ * the next middleware or origin handler.
17
+ *
18
+ * @param path - The URL path to intercept for WebSocket upgrades. Defaults to `/`.
19
+ * Supports dynamic segments using `path-to-regexp` syntax.
20
+ * @returns A {@link Middleware} instance that can be used in your middleware chain.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * app.use(websocket("/chat/:name"));
25
+ * ```
26
+ */
27
+ declare function websocket(path?: string): Middleware;
28
+
29
+ export { websocket };
package/dist/websocket.js CHANGED
@@ -1,2 +1,2 @@
1
- import {match}from'path-to-regexp';import {StatusCodes}from'http-status-codes/build/es/status-codes';import {getReasonPhrase}from'http-status-codes/build/es/utils-functions';import R from'cache-control-parser';var g=(n=>(n.GET="GET",n.PUT="PUT",n.HEAD="HEAD",n.POST="POST",n.PATCH="PATCH",n.DELETE="DELETE",n.OPTIONS="OPTIONS",n))(g||{}),{GET:I,PUT:V,HEAD:v,POST:K,PATCH:q,DELETE:$,OPTIONS:z}=g;var E={parse:R.parse,stringify:R.stringify,DISABLE:Object.freeze({"no-cache":true,"no-store":true,"must-revalidate":true,"max-age":0})};var o={CACHE_CONTROL:"cache-control",CONNECTION:"connection",CONTENT_DISPOSITION:"content-disposition",CONTENT_ENCODING:"content-encoding",CONTENT_LANGUAGE:"content-language",CONTENT_LENGTH:"content-length",CONTENT_RANGE:"content-range",CONTENT_TYPE:"content-type",CONTENT_MD5:"content-md5",SEC_WEBSOCKET_VERSION:"sec-websocket-version",UPGRADE:"upgrade"},H=[o.CONTENT_TYPE,o.CONTENT_LENGTH,o.CONTENT_RANGE,o.CONTENT_ENCODING,o.CONTENT_LANGUAGE,o.CONTENT_DISPOSITION,o.CONTENT_MD5],D=[o.CONTENT_LENGTH,o.CONTENT_RANGE];var L="upgrade",P="websocket";var O="utf-8";function h(e,t){return e<t?-1:e>t?1:0}function m(e,t,r){let s=Array.isArray(r)?r:[r],i=Array.from(new Set(s.map(c=>c.trim()))).filter(c=>c.length).sort(h);if(!i.length){e.delete(t);return}e.set(t,i.join(", "));}function b(e,t,r){let s=Array.isArray(r)?r:[r];if(s.length===0)return;let c=d(e,t).concat(s.map(M=>M.trim()));m(e,t,c);}function d(e,t){let r=e.get(t)?.split(",").map(s=>s.trim()).filter(s=>s.length>0)??[];return Array.from(new Set(r)).sort(h)}function N(e,t){for(let r of t)e.delete(r);}function f(e,t){return !t||e.toLowerCase().includes("charset=")?e:`${e}; charset=${t.toLowerCase()}`}var _=class{headers=new Headers;status=StatusCodes.OK;statusText;webSocket;mediaType=f("text/plain",O);get responseInit(){return {headers:this.headers,status:this.status,statusText:this.statusText??getReasonPhrase(this.status),webSocket:this.webSocket,encodeBody:"automatic"}}setHeader(t,r){m(this.headers,t,r);}mergeHeader(t,r){b(this.headers,t,r);}addContentType(){this.headers.get(o.CONTENT_TYPE)||this.setHeader(o.CONTENT_TYPE,this.mediaType);}filterHeaders(){this.status===StatusCodes.NO_CONTENT?N(this.headers,D):this.status===StatusCodes.NOT_MODIFIED&&N(this.headers,H);}},S=class extends _{constructor(r){super();this.cache=r;}addCacheHeader(){this.cache&&this.setHeader(o.CACHE_CONTROL,E.stringify(this.cache));}},A=class extends S{constructor(r=null,s){super(s);this.body=r;}async response(){this.addCacheHeader();let r=[StatusCodes.NO_CONTENT,StatusCodes.NOT_MODIFIED].includes(this.status)?null:this.body;return r&&this.addContentType(),this.filterHeaders(),new Response(r,this.responseInit)}};var x=class extends A{constructor(t=null,r,s=StatusCodes.OK){super(t,r),this.status=s;}},u=class extends x{constructor(t={},r,s=StatusCodes.OK){super(JSON.stringify(t),r,s),this.mediaType=f("application/json",O);}};var T=class extends u{constructor(r,s){let i={status:r,error:getReasonPhrase(r),details:s??""};super(i,E.DISABLE,r);this.details=s;}};var p=class extends T{constructor(t){super(StatusCodes.BAD_REQUEST,t);}};var l=class extends T{constructor(){super(StatusCodes.UPGRADE_REQUIRED),this.setHeader(o.SEC_WEBSOCKET_VERSION,"13");}};function G(e){return d(e,o.CONNECTION).some(t=>t.toLowerCase()===L)}function W(e){return d(e,o.UPGRADE).some(t=>t.toLowerCase()===P)}function U(e){return e.get(o.SEC_WEBSOCKET_VERSION)?.trim()==="13"}var C=class{constructor(t){this.path=t;}handle(t,r){if(t.request.method!==I||!this.isMatch(t.request))return r();let s=t.request.headers;return G(s)?W(s)?U(s)?r():new l().response():new p("Missing or invalid 'Upgrade' header").response():new p("Missing or invalid 'Connection' header").response()}isMatch(t){return match(this.path)(new URL(t.url).pathname)!==false}};function Dt(e="/"){return new C(e)}export{Dt as websocket};//# sourceMappingURL=websocket.js.map
1
+ import {match}from'path-to-regexp';import {getReasonPhrase}from'http-status-codes/build/es/utils-functions';import {StatusCodes}from'http-status-codes/build/es/status-codes';import R from'cache-control-parser';var g=(a=>(a.GET="GET",a.PUT="PUT",a.HEAD="HEAD",a.POST="POST",a.PATCH="PATCH",a.DELETE="DELETE",a.OPTIONS="OPTIONS",a))(g||{}),{GET:I,PUT:F,HEAD:V,POST:v,PATCH:K,DELETE:q,OPTIONS:$}=g;var E={parse:R.parse,stringify:R.stringify,DISABLE:Object.freeze({"no-cache":true,"no-store":true,"must-revalidate":true,"max-age":0})};var o={CACHE_CONTROL:"cache-control",CONNECTION:"connection",CONTENT_DISPOSITION:"content-disposition",CONTENT_ENCODING:"content-encoding",CONTENT_LANGUAGE:"content-language",CONTENT_LENGTH:"content-length",CONTENT_RANGE:"content-range",CONTENT_TYPE:"content-type",CONTENT_MD5:"content-md5",SEC_WEBSOCKET_VERSION:"sec-websocket-version",UPGRADE:"upgrade"},H=[o.CONTENT_TYPE,o.CONTENT_LENGTH,o.CONTENT_RANGE,o.CONTENT_ENCODING,o.CONTENT_LANGUAGE,o.CONTENT_DISPOSITION,o.CONTENT_MD5],D=[o.CONTENT_LENGTH,o.CONTENT_RANGE];var L="upgrade",P="websocket";var m="utf-8";function O(e,t){return e<t?-1:e>t?1:0}function h(e,t,r){let s=Array.isArray(r)?r:[r],i=Array.from(new Set(s.map(c=>c.trim()))).filter(c=>c.length).sort(O);if(!i.length){e.delete(t);return}e.set(t,i.join(", "));}function b(e,t,r){let s=Array.isArray(r)?r:[r];if(s.length===0)return;let c=d(e,t).concat(s.map(U=>U.trim()));h(e,t,c);}function d(e,t){let r=e.get(t)?.split(",").map(s=>s.trim()).filter(s=>s.length>0)??[];return Array.from(new Set(r)).sort(O)}function N(e,t){for(let r of t)e.delete(r);}function f(e,t){return !t||e.toLowerCase().includes("charset=")?e:`${e}; charset=${t.toLowerCase()}`}var _=class{headers=new Headers;status=StatusCodes.OK;statusText;webSocket;mediaType=f("text/plain",m);get responseInit(){return {headers:this.headers,status:this.status,statusText:this.statusText??getReasonPhrase(this.status),webSocket:this.webSocket,encodeBody:"automatic"}}setHeader(t,r){h(this.headers,t,r);}mergeHeader(t,r){b(this.headers,t,r);}addContentType(){this.headers.get(o.CONTENT_TYPE)||this.setHeader(o.CONTENT_TYPE,this.mediaType);}filterHeaders(){this.status===StatusCodes.NO_CONTENT?N(this.headers,D):this.status===StatusCodes.NOT_MODIFIED&&N(this.headers,H);}},S=class extends _{constructor(r){super();this.cache=r;}addCacheHeader(){this.cache&&this.setHeader(o.CACHE_CONTROL,E.stringify(this.cache));}},x=class extends S{constructor(r=null,s){super(s);this.body=r;}async response(){this.addCacheHeader();let r=[StatusCodes.NO_CONTENT,StatusCodes.NOT_MODIFIED].includes(this.status)?null:this.body;return r&&this.addContentType(),this.filterHeaders(),new Response(r,this.responseInit)}};var A=class extends x{constructor(t=null,r,s=StatusCodes.OK){super(t,r),this.status=s;}},u=class extends A{constructor(t={},r,s=StatusCodes.OK){super(JSON.stringify(t),r,s),this.mediaType=f("application/json",m);}};var T=class extends u{constructor(r,s){let i={status:r,error:getReasonPhrase(r),details:s??""};super(i,E.DISABLE,r);this.details=s;}};var p=class extends T{constructor(t){super(StatusCodes.BAD_REQUEST,t);}};var l=class extends T{constructor(){super(StatusCodes.UPGRADE_REQUIRED),this.setHeader(o.SEC_WEBSOCKET_VERSION,"13");}};function y(e){return d(e,o.CONNECTION).some(t=>t.toLowerCase()===L)}function G(e){return d(e,o.UPGRADE).some(t=>t.toLowerCase()===P)}function W(e){return e.get(o.SEC_WEBSOCKET_VERSION)?.trim()==="13"}var C=class{constructor(t){this.path=t;}handle(t,r){if(t.request.method!==I||!this.isMatch(t.request))return r();let s=t.request.headers;return y(s)?G(s)?W(s)?r():new l().response():new p("Missing or invalid 'Upgrade' header").response():new p("Missing or invalid 'Connection' header").response()}isMatch(t){return match(this.path)(new URL(t.url).pathname)!==false}};function wt(e="/"){return new C(e)}export{wt as websocket};//# sourceMappingURL=websocket.js.map
2
2
  //# sourceMappingURL=websocket.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/constants/methods.ts","../src/constants/cache.ts","../src/constants/headers.ts","../src/middleware/websocket/constants.ts","../src/constants/media.ts","../src/utils/compare.ts","../src/utils/headers.ts","../src/utils/media.ts","../src/responses.ts","../src/errors.ts","../src/middleware/websocket/utils.ts","../src/middleware/websocket/handler.ts","../src/middleware/websocket/websocket.ts"],"names":["Method","GET","PUT","HEAD","POST","PATCH","DELETE","OPTIONS","CacheControl","CacheLib","HttpHeader","FORBIDDEN_304_HEADERS","FORBIDDEN_204_HEADERS","WS_UPGRADE","WS_WEBSOCKET","UTF8_CHARSET","lexCompare","a","b","setHeader","headers","key","value","raw","values","v","mergeHeader","merged","getHeaderValues","filterHeaders","keys","withCharset","mediaType","charset","BaseResponse","StatusCodes","getReasonPhrase","CacheResponse","cache","WorkerResponse","body","SuccessResponse","status","JsonResponse","json","HttpError","details","BadRequest","UpgradeRequired","hasConnectionHeader","hasUpgradeHeader","hasWebSocketVersion","WebSocketHandler","path","worker","next","request","match","websocket"],"mappings":"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","file":"websocket.js","sourcesContent":["/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Standard HTTP request methods.\n */\nexport enum Method {\n GET = \"GET\",\n PUT = \"PUT\",\n HEAD = \"HEAD\",\n POST = \"POST\",\n PATCH = \"PATCH\",\n DELETE = \"DELETE\",\n OPTIONS = \"OPTIONS\",\n}\n\n/**\n * Shorthand constants for each HTTP method.\n *\n * These are equivalent to the corresponding enum members in `Method`.\n * For example, `GET === Method.GET`.\n */\nexport const { GET, PUT, HEAD, POST, PATCH, DELETE, OPTIONS } = Method;\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport CacheLib from \"cache-control-parser\";\n\n/**\n * @see {@link https://github.com/etienne-martin/cache-control-parser | cache-control-parser}\n */\nexport type CacheControl = CacheLib.CacheControl;\nexport const CacheControl = {\n parse: CacheLib.parse,\n stringify: CacheLib.stringify,\n\n /** A CacheControl directive that disables all caching. */\n DISABLE: Object.freeze({\n \"no-cache\": true,\n \"no-store\": true,\n \"must-revalidate\": true,\n \"max-age\": 0,\n }) satisfies CacheControl,\n};\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Internally used headers.\n */\nexport const HttpHeader = {\n ACCEPT: \"accept\",\n ACCEPT_ENCODING: \"accept-encoding\",\n ACCEPT_LANGUAGE: \"accept-language\",\n ACCEPT_RANGES: \"accept-ranges\",\n ALLOW: \"allow\",\n AUTHORIZATION: \"authorization\",\n CACHE_CONTROL: \"cache-control\",\n CONNECTION: \"connection\",\n CONTENT_DISPOSITION: \"content-disposition\",\n CONTENT_ENCODING: \"content-encoding\",\n CONTENT_LANGUAGE: \"content-language\",\n CONTENT_LENGTH: \"content-length\",\n CONTENT_RANGE: \"content-range\",\n CONTENT_TYPE: \"content-type\",\n CONTENT_MD5: \"content-md5\",\n COOKIE: \"cookie\",\n ETAG: \"etag\",\n IF_MATCH: \"if-match\",\n IF_MODIFIED_SINCE: \"if-modified-since\",\n IF_NONE_MATCH: \"if-none-match\",\n IF_UNMODIFIED_SINCE: \"if-unmodified-since\",\n LAST_MODIFIED: \"last-modified\",\n ORIGIN: \"origin\",\n RANGE: \"range\",\n SET_COOKIE: \"set-cookie\",\n VARY: \"vary\",\n\n // Cors Headers\n ACCESS_CONTROL_ALLOW_CREDENTIALS: \"access-control-allow-credentials\",\n ACCESS_CONTROL_ALLOW_HEADERS: \"access-control-allow-headers\",\n ACCESS_CONTROL_ALLOW_METHODS: \"access-control-allow-methods\",\n ACCESS_CONTROL_ALLOW_ORIGIN: \"access-control-allow-origin\",\n ACCESS_CONTROL_EXPOSE_HEADERS: \"access-control-expose-headers\",\n ACCESS_CONTROL_MAX_AGE: \"access-control-max-age\",\n\n // Websocket Headers\n SEC_WEBSOCKET_VERSION: \"sec-websocket-version\",\n UPGRADE: \"upgrade\",\n\n // Internal Headers\n INTERNAL_VARIANT_SET: \"internal-variant-set\",\n} as const;\n\n/**\n * Headers that must not be sent in 304 Not Modified responses.\n * These are stripped to comply with the HTTP spec.\n */\nexport const FORBIDDEN_304_HEADERS = [\n HttpHeader.CONTENT_TYPE,\n HttpHeader.CONTENT_LENGTH,\n HttpHeader.CONTENT_RANGE,\n HttpHeader.CONTENT_ENCODING,\n HttpHeader.CONTENT_LANGUAGE,\n HttpHeader.CONTENT_DISPOSITION,\n HttpHeader.CONTENT_MD5,\n];\n\n/**\n * Headers that should not be sent in 204 No Content responses.\n * Stripping them is recommended but optional per spec.\n */\nexport const FORBIDDEN_204_HEADERS = [HttpHeader.CONTENT_LENGTH, HttpHeader.CONTENT_RANGE];\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/** WebSocket upgrade header value */\nexport const WS_UPGRADE = \"upgrade\";\n\n/** WebSocket protocol header value */\nexport const WS_WEBSOCKET = \"websocket\";\n\n/** WebSocket protocol version */\nexport const WS_VERSION = \"13\";\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const UTF8_CHARSET = \"utf-8\";\n\n/**\n * Internal media types.\n */\nexport enum MediaType {\n PLAIN_TEXT = \"text/plain\",\n HTML = \"text/html\",\n JSON = \"application/json\",\n OCTET_STREAM = \"application/octet-stream\",\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Lexicographically compares two strings.\n *\n * This comparator can be used in `Array.prototype.sort()` to produce a\n * consistent, stable ordering of string arrays.\n *\n * @param a - The first string to compare.\n * @param b - The second string to compare.\n * @returns A number indicating the relative order of `a` and `b`.\n */\nexport function lexCompare(a: string, b: string): number {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { lexCompare } from \"./compare\";\n\n/**\n * Sets a header on the given Headers object.\n *\n * - If `value` is an array, any duplicates and empty strings are removed.\n * - If the resulting value is empty, the header is deleted.\n * - Otherwise, values are joined with `\", \"` and set as the header value.\n *\n * @param headers - The Headers object to modify.\n * @param key - The header name to set.\n * @param value - The header value(s) to set. Can be a string or array of strings.\n */\nexport function setHeader(headers: Headers, key: string, value: string | string[]): void {\n const raw = Array.isArray(value) ? value : [value];\n const values = Array.from(new Set(raw.map((v) => v.trim())))\n .filter((v) => v.length)\n .sort(lexCompare);\n\n if (!values.length) {\n headers.delete(key);\n return;\n }\n\n headers.set(key, values.join(\", \"));\n}\n\n/**\n * Merges new value(s) into an existing header on the given Headers object.\n *\n * - Preserves any existing values and adds new ones.\n * - Removes duplicates and trims all values.\n * - If the header does not exist, it is created.\n * - If the resulting value array is empty, the header is deleted.\n *\n * @param headers - The Headers object to modify.\n * @param key - The header name to merge into.\n * @param value - The new header value(s) to add. Can be a string or array of strings.\n */\nexport function mergeHeader(headers: Headers, key: string, value: string | string[]): void {\n const values = Array.isArray(value) ? value : [value];\n if (values.length === 0) return;\n\n const existing = getHeaderValues(headers, key);\n const merged = existing.concat(values.map((v) => v.trim()));\n\n setHeader(headers, key, merged);\n}\n\n/**\n * Returns the values of an HTTP header as an array of strings.\n *\n * This helper:\n * - Retrieves the header value by `key`.\n * - Splits the value on commas.\n * - Trims surrounding whitespace from each entry.\n * - Filters out any empty tokens.\n * - Removes duplicate values (case-sensitive)\n *\n * If the header is not present, an empty array is returned.\n *\n */\nexport function getHeaderValues(headers: Headers, key: string): string[] {\n const values =\n headers\n .get(key)\n ?.split(\",\")\n .map((v) => v.trim())\n .filter((v) => v.length > 0) ?? [];\n return Array.from(new Set(values)).sort(lexCompare);\n}\n\n/**\n * Removes a list of header fields from a {@link Headers} object.\n *\n * @param headers - The {@link Headers} object to modify in place.\n * @param keys - An array of header field names to remove. Header names are\n * matched case-insensitively per the Fetch spec.\n */\nexport function filterHeaders(headers: Headers, keys: string[]): void {\n for (const key of keys) {\n headers.delete(key);\n }\n}\n\n/**\n * Extracts all header names from a `Headers` object, normalizes them,\n * and returns them in a stable, lexicographically sorted array.\n *\n * @param headers - The `Headers` object to extract keys from.\n * @returns A sorted array of lowercase header names.\n */\nexport function getHeaderKeys(headers: Headers): string[] {\n return [...headers.keys()].sort(lexCompare);\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Appends a charset parameter to a given media type string,\n * avoiding duplicates and ignoring empty charsets.\n *\n * @param {string} mediaType - The MIME type (e.g., \"text/html\").\n * @param {string} charset - The character set to append (e.g., \"utf-8\").\n * @returns {string} The media type with charset appended if provided.\n */\nexport function withCharset(mediaType: string, charset: string): string {\n if (!charset || mediaType.toLowerCase().includes(\"charset=\")) {\n return mediaType;\n }\n return `${mediaType}; charset=${charset.toLowerCase()}`;\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { StatusCodes } from \"http-status-codes/build/es/status-codes\";\nimport { getReasonPhrase } from \"http-status-codes/build/es/utils-functions\";\n\nimport { CacheControl } from \"./constants/cache\";\nimport { FORBIDDEN_204_HEADERS, FORBIDDEN_304_HEADERS, HttpHeader } from \"./constants/headers\";\nimport { MediaType, UTF8_CHARSET } from \"./constants/media\";\nimport { GET, HEAD } from \"./constants/methods\";\nimport { assertMethods } from \"./guards/methods\";\nimport { assertOctetStreamInit } from \"./guards/responses\";\nimport { Worker } from \"./interfaces\";\nimport { OctetStreamInit } from \"./interfaces/response\";\nimport { filterHeaders, mergeHeader, setHeader } from \"./utils/headers\";\nimport { withCharset } from \"./utils/media\";\n\n/**\n * Base class for building HTTP responses.\n * Manages headers, status, and media type.\n */\nabstract class BaseResponse {\n /** HTTP headers for the response. */\n public headers: Headers = new Headers();\n\n /** HTTP status code (default 200 OK). */\n public status: StatusCodes = StatusCodes.OK;\n\n /** Optional status text. Defaults to standard reason phrase. */\n public statusText?: string;\n\n /** Optional websocket property. */\n public webSocket?: WebSocket | null;\n\n /** Default media type of the response body. */\n public mediaType: string = withCharset(MediaType.PLAIN_TEXT, UTF8_CHARSET);\n\n /** Converts current state to ResponseInit for constructing a Response. */\n protected get responseInit(): ResponseInit {\n return {\n headers: this.headers,\n status: this.status,\n statusText: this.statusText ?? getReasonPhrase(this.status),\n webSocket: this.webSocket,\n encodeBody: \"automatic\",\n };\n }\n\n /** Sets a header, overwriting any existing value. */\n public setHeader(key: string, value: string | string[]): void {\n setHeader(this.headers, key, value);\n }\n\n /** Merges a header with existing values (does not overwrite). */\n public mergeHeader(key: string, value: string | string[]): void {\n mergeHeader(this.headers, key, value);\n }\n\n /** Adds a Content-Type header if not already existing (does not overwrite). */\n public addContentType() {\n if (!this.headers.get(HttpHeader.CONTENT_TYPE)) {\n this.setHeader(HttpHeader.CONTENT_TYPE, this.mediaType);\n }\n }\n\n /**\n * Removes headers that are disallowed or discouraged based on the current\n * status code.\n *\n * - **204 No Content:** strips headers that \"should not\" be sent\n * (`Content-Length`, `Content-Range`), per the HTTP spec.\n * - **304 Not Modified:** strips headers that \"must not\" be sent\n * (`Content-Type`, `Content-Length`, `Content-Range`, etc.), per the HTTP spec.\n *\n * This ensures that responses remain compliant with HTTP/1.1 standards while preserving\n * custom headers that are allowed.\n */\n public filterHeaders(): void {\n if (this.status === StatusCodes.NO_CONTENT) {\n filterHeaders(this.headers, FORBIDDEN_204_HEADERS);\n } else if (this.status === StatusCodes.NOT_MODIFIED) {\n filterHeaders(this.headers, FORBIDDEN_304_HEADERS);\n }\n }\n}\n\n/**\n * Base response class that adds caching headers.\n */\nabstract class CacheResponse extends BaseResponse {\n constructor(public cache?: CacheControl) {\n super();\n }\n\n /** Adds Cache-Control header if caching is configured. */\n protected addCacheHeader(): void {\n if (this.cache) {\n this.setHeader(HttpHeader.CACHE_CONTROL, CacheControl.stringify(this.cache));\n }\n }\n}\n\n/**\n * Core response. Combines caching, and content type headers.\n */\nexport abstract class WorkerResponse extends CacheResponse {\n constructor(\n private readonly body: BodyInit | null = null,\n cache?: CacheControl,\n ) {\n super(cache);\n }\n\n /** Builds the Response with body, headers, and status. */\n public async response(): Promise<Response> {\n this.addCacheHeader();\n\n const body = [StatusCodes.NO_CONTENT, StatusCodes.NOT_MODIFIED].includes(this.status)\n ? null\n : this.body;\n\n if (body) this.addContentType();\n\n this.filterHeaders();\n\n return new Response(body, this.responseInit);\n }\n}\n\n/**\n * Copies an existing response for mutation. Pass in a CacheControl\n * to be used for the response, overriding any existing `cache-control`\n * on the source response.\n */\nexport class CopyResponse extends WorkerResponse {\n constructor(response: Response, cache?: CacheControl) {\n super(response.body, cache);\n this.status = response.status;\n this.statusText = response.statusText;\n this.headers = new Headers(response.headers);\n }\n}\n\n/**\n * Copies the response, but with null body and status 304 Not Modified.\n */\nexport class NotModified extends WorkerResponse {\n constructor(response: Response) {\n super();\n this.status = StatusCodes.NOT_MODIFIED;\n this.headers = new Headers(response.headers);\n }\n}\n\n/**\n * Represents a successful response with customizable body, cache and status.\n */\nexport class SuccessResponse extends WorkerResponse {\n constructor(\n body: BodyInit | null = null,\n cache?: CacheControl,\n status: StatusCodes = StatusCodes.OK,\n ) {\n super(body, cache);\n this.status = status;\n }\n}\n\n/**\n * JSON response. Automatically sets Content-Type to application/json.\n */\nexport class JsonResponse extends SuccessResponse {\n constructor(json: unknown = {}, cache?: CacheControl, status: StatusCodes = StatusCodes.OK) {\n super(JSON.stringify(json), cache, status);\n this.mediaType = withCharset(MediaType.JSON, UTF8_CHARSET);\n }\n}\n\n/**\n * HTML response. Automatically sets Content-Type to text/html.\n */\nexport class HtmlResponse extends SuccessResponse {\n constructor(\n body: string,\n cache?: CacheControl,\n status: StatusCodes = StatusCodes.OK,\n charset: string = UTF8_CHARSET,\n ) {\n super(body, cache, status);\n this.mediaType = withCharset(MediaType.HTML, charset);\n }\n}\n\n/**\n * Plain text response. Automatically sets Content-Type to text/plain.\n */\nexport class TextResponse extends SuccessResponse {\n constructor(\n body: string,\n cache?: CacheControl,\n status: StatusCodes = StatusCodes.OK,\n charset: string = UTF8_CHARSET,\n ) {\n super(body, cache, status);\n this.mediaType = withCharset(MediaType.PLAIN_TEXT, charset);\n }\n}\n\n/**\n * Represents an HTTP response for serving binary data as `application/octet-stream`.\n *\n * This class wraps a `ReadableStream` and sets all necessary headers for both\n * full and partial content responses, handling range requests in a hybrid way\n * to maximize browser and CDN caching.\n *\n * Key behaviors:\n * - `Content-Type` is set to `application/octet-stream`.\n * - `Accept-Ranges: bytes` is always included.\n * - `Content-Length` is always set to the validated length of the response body.\n * - If the request is a true partial range (offset > 0 or length < size), the response\n * will be `206 Partial Content` with the appropriate `Content-Range` header.\n * - If the requested range covers the entire file (even if a Range header is present),\n * the response will return `200 OK` to enable browser and edge caching.\n * - Zero-length streams (`size = 0`) are never treated as partial.\n * - Special case: a requested range of `0-0` on a non-empty file is normalized to 1 byte.\n */\nexport class OctetStream extends WorkerResponse {\n constructor(stream: ReadableStream, init: OctetStreamInit, cache?: CacheControl) {\n assertOctetStreamInit(init);\n\n super(stream, cache);\n this.mediaType = MediaType.OCTET_STREAM;\n\n const normalized = OctetStream.normalizeInit(init);\n const { size, offset, length } = normalized;\n\n if (OctetStream.isPartial(normalized)) {\n this.setHeader(\n HttpHeader.CONTENT_RANGE,\n `bytes ${offset}-${offset + length - 1}/${size}`,\n );\n this.status = StatusCodes.PARTIAL_CONTENT;\n }\n\n this.setHeader(HttpHeader.ACCEPT_RANGES, \"bytes\");\n this.setHeader(HttpHeader.CONTENT_LENGTH, `${length}`);\n }\n\n /**\n * Normalizes a partially-specified `OctetStreamInit` into a fully-specified object.\n *\n * Ensures that all required fields (`size`, `offset`, `length`) are defined:\n * - `offset` defaults to 0 if not provided.\n * - `length` defaults to `size - offset` if not provided.\n * - Special case: if `offset` and `length` are both 0 but `size > 0`, `length` is set to 1\n * to avoid zero-length partial streams.\n *\n * @param init - The initial `OctetStreamInit` object, possibly with missing `offset` or `length`.\n * @returns A fully-specified `OctetStreamInit` object with `size`, `offset`, and `length` guaranteed.\n */\n private static normalizeInit(init: OctetStreamInit): Required<OctetStreamInit> {\n const { size } = init;\n const offset = init.offset ?? 0;\n let length = init.length ?? size - offset;\n\n if (offset === 0 && length === 0 && size > 0) {\n length = 1;\n }\n\n return { size, offset, length };\n }\n\n /**\n * Determines whether the given `OctetStreamInit` represents a partial range.\n *\n * Partial ranges are defined as any range that does **not** cover the entire file:\n * - If `size === 0`, the stream is never partial.\n * - If `offset === 0` and `length === size`, the stream is treated as a full file (not partial),\n * even if a Range header is present. This enables browser and CDN caching.\n * - All other cases are considered partial, and will result in a `206 Partial Content` response.\n *\n * @param init - A fully-normalized `OctetStreamInit` object.\n * @returns `true` if the stream represents a partial range; `false` if it represents the full file.\n */\n private static isPartial(init: Required<OctetStreamInit>): boolean {\n if (init.size === 0) return false;\n return !(init.offset === 0 && init.length === init.size);\n }\n}\n\n/**\n * A streaming response for Cloudflare R2 objects.\n *\n * **Partial content support:** To enable HTTP 206 streaming, you must provide\n * request headers containing the `Range` header when calling the R2 bucket's `get()` method.\n *\n * Example:\n * ```ts\n * const stream = await this.env.R2_BUCKET.get(\"key\", { range: this.request.headers });\n * ```\n *\n * @param source - The R2 object to stream.\n * @param cache - Optional caching override.\n */\nexport class R2ObjectStream extends OctetStream {\n constructor(source: R2ObjectBody, cache?: CacheControl) {\n let useCache = cache;\n if (!useCache && source.httpMetadata?.cacheControl) {\n useCache = CacheControl.parse(source.httpMetadata.cacheControl);\n }\n\n super(source.body, R2ObjectStream.computeRange(source.size, source.range), useCache);\n\n this.setHeader(HttpHeader.ETAG, source.httpEtag);\n\n if (source.httpMetadata?.contentType) {\n this.mediaType = source.httpMetadata.contentType;\n }\n }\n\n /**\n * Computes an `OctetStreamInit` object from a given R2 range.\n *\n * This function normalizes a Cloudflare R2 `R2Range` into the shape expected\n * by `OctetStream`. It handles the following cases:\n *\n * - No range provided: returns `{ size }` (full content).\n * - `suffix` range: calculates the offset and length from the end of the file.\n * - Explicit `offset` and/or `length`: passed through as-is.\n *\n * @param size - The total size of the file/object.\n * @param range - Optional range to extract (from R2). Can be:\n * - `{ offset: number; length?: number }`\n * - `{ offset?: number; length: number }`\n * - `{ suffix: number }`\n * @returns An `OctetStreamInit` object suitable for `OctetStream`.\n */\n private static computeRange(size: number, range?: R2Range): OctetStreamInit {\n if (!range) return { size };\n\n if (\"suffix\" in range) {\n const offset = Math.max(0, size - range.suffix);\n const length = size - offset;\n return { size, offset, length };\n }\n\n return { size, ...range };\n }\n}\n\n/**\n * Response for WebSocket upgrade requests.\n * Automatically sets status to 101 and attaches the client socket.\n */\nexport class WebSocketUpgrade extends WorkerResponse {\n constructor(client: WebSocket) {\n super();\n this.status = StatusCodes.SWITCHING_PROTOCOLS;\n this.webSocket = client;\n }\n}\n\n/**\n * Response for `HEAD` requests. Copy headers and status from a `GET` response\n * without the body.\n */\nexport class Head extends WorkerResponse {\n constructor(get: Response) {\n super();\n this.status = get.status;\n this.statusText = get.statusText;\n this.headers = new Headers(get.headers);\n }\n}\n\n/**\n * Response for `OPTIONS` requests.\n */\nexport class Options extends WorkerResponse {\n constructor(worker: Worker) {\n const allowed = Array.from(new Set([GET, HEAD, ...worker.getAllowedMethods()]));\n assertMethods(allowed);\n\n super();\n this.status = StatusCodes.NO_CONTENT;\n this.setHeader(HttpHeader.ALLOW, allowed);\n }\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { StatusCodes } from \"http-status-codes/build/es/status-codes\";\nimport { getReasonPhrase } from \"http-status-codes/build/es/utils-functions\";\n\nimport { CacheControl } from \"./constants/cache\";\nimport { HttpHeader } from \"./constants/headers\";\nimport { assertMethods } from \"./guards/methods\";\nimport { ErrorJson } from \"./interfaces/error\";\nimport { Worker } from \"./interfaces/worker\";\nimport { WS_VERSION } from \"./middleware/websocket/constants\";\nimport { JsonResponse } from \"./responses\";\n\n/**\n * Generic HTTP error response.\n * Sends a JSON body with status, error message, and details.\n */\nexport class HttpError extends JsonResponse {\n /**\n * @param worker The worker handling the request.\n * @param status HTTP status code.\n * @param details Optional detailed error message.\n */\n constructor(\n status: StatusCodes,\n protected readonly details?: string,\n ) {\n const json: ErrorJson = {\n status,\n error: getReasonPhrase(status),\n details: details ?? \"\",\n };\n super(json, CacheControl.DISABLE, status);\n }\n}\n\n/**\n * Creates a structured error response without exposing the error\n * details to the client. Links the sent response to the logged\n * error via a generated correlation ID.\n *\n * Status defaults to 500 Internal Server Error.\n */\nexport class LoggedHttpError extends HttpError {\n constructor(error: unknown, status: StatusCodes = StatusCodes.INTERNAL_SERVER_ERROR) {\n const uuid = crypto.randomUUID();\n console.error(uuid, error);\n super(status, uuid);\n }\n}\n\n/** 400 Bad Request error response. */\nexport class BadRequest extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.BAD_REQUEST, details);\n }\n}\n\n/** 401 Unauthorized error response. */\nexport class Unauthorized extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.UNAUTHORIZED, details);\n }\n}\n\n/** 403 Forbidden error response. */\nexport class Forbidden extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.FORBIDDEN, details);\n }\n}\n\n/** 404 Not Found error response. */\nexport class NotFound extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.NOT_FOUND, details);\n }\n}\n\n/** 405 Method Not Allowed error response. */\nexport class MethodNotAllowed extends HttpError {\n constructor(worker: Worker) {\n const methods = worker.getAllowedMethods();\n assertMethods(methods);\n\n super(StatusCodes.METHOD_NOT_ALLOWED, `${worker.request.method} method not allowed.`);\n this.setHeader(HttpHeader.ALLOW, methods);\n }\n}\n\n/** 412 Precondition Failed error response */\nexport class PreconditionFailed extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.PRECONDITION_FAILED, details);\n }\n}\n\n/** 426 Upgrade Required error response. */\nexport class UpgradeRequired extends HttpError {\n constructor() {\n super(StatusCodes.UPGRADE_REQUIRED);\n this.setHeader(HttpHeader.SEC_WEBSOCKET_VERSION, WS_VERSION);\n }\n}\n\n/** 500 Internal Server Error response. */\nexport class InternalServerError extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.INTERNAL_SERVER_ERROR, details);\n }\n}\n\n/** 501 Not Implemented error response. */\nexport class NotImplemented extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.NOT_IMPLEMENTED, details);\n }\n}\n\n/** 501 Method Not Implemented error response for unsupported HTTP methods. */\nexport class MethodNotImplemented extends NotImplemented {\n constructor(worker: Worker) {\n super(`${worker.request.method} method not implemented.`);\n }\n}\n\n/** 503 Service Unavailable error response. */\nexport class ServiceUnavailable extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.SERVICE_UNAVAILABLE, details);\n }\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { HttpHeader } from \"../../constants/headers\";\nimport { getHeaderValues } from \"../../utils/headers\";\n\nimport { WS_UPGRADE, WS_VERSION, WS_WEBSOCKET } from \"./constants\";\n\n/**\n * Checks if the `Connection` header includes the WebSocket upgrade token.\n *\n * @param headers - The Headers object to inspect.\n * @returns `true` if a WebSocket upgrade is requested via `Connection` header, `false` otherwise.\n */\nexport function hasConnectionHeader(headers: Headers): boolean {\n return getHeaderValues(headers, HttpHeader.CONNECTION).some(\n (value) => value.toLowerCase() === WS_UPGRADE,\n );\n}\n\n/**\n * Checks if the `Upgrade` header requests a WebSocket upgrade.\n *\n * @param headers - The Headers object to inspect.\n * @returns `true` if the `Upgrade` header is set to `websocket`, `false` otherwise.\n */\nexport function hasUpgradeHeader(headers: Headers): boolean {\n return getHeaderValues(headers, HttpHeader.UPGRADE).some(\n (value) => value.toLowerCase() === WS_WEBSOCKET,\n );\n}\n\n/**\n * Determines whether the request includes the correct WebSocket version.\n *\n * @param headers - The Headers object to inspect.\n * @returns `true` if `Sec-WebSocket-Version` matches the expected version, `false` otherwise.\n */\nexport function hasWebSocketVersion(headers: Headers): boolean {\n return headers.get(HttpHeader.SEC_WEBSOCKET_VERSION)?.trim() === WS_VERSION;\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { match } from \"path-to-regexp\";\n\nimport { GET } from \"../../constants/methods\";\nimport { BadRequest, UpgradeRequired } from \"../../errors\";\nimport { Middleware } from \"../../interfaces/middleware\";\nimport { Worker } from \"../../interfaces/worker\";\n\nimport { hasConnectionHeader, hasUpgradeHeader, hasWebSocketVersion } from \"./utils\";\n\n/**\n * Middleware for validating WebSocket upgrade requests.\n *\n * - Only applies to `GET` requests.\n * - Matches requests against a specific path using `path-to-regexp` patterns.\n * - Validates that the request contains required WebSocket headers:\n * - `Connection: Upgrade`\n * - `Upgrade: websocket`\n * - `Sec-WebSocket-Version` matches the expected version\n * - Returns an error response if any validation fails.\n * - Otherwise, passes control to the next middleware or origin handler.\n */\nexport class WebSocketHandler implements Middleware {\n /**\n * Creates a new WebSocketHandler for a specific path.\n *\n * @param path - The request path this handler should intercept for WebSocket upgrades.\n * Supports dynamic segments using `path-to-regexp` syntax.\n */\n constructor(private readonly path: string) {}\n\n /**\n * Handles an incoming request, validating WebSocket upgrade headers.\n *\n * @param worker - The Worker instance containing the request.\n * @param next - Function to invoke the next middleware.\n * @returns A Response object if the request fails WebSocket validation,\n * or the result of `next()` if the request is valid or does not match.\n */\n public handle(worker: Worker, next: () => Promise<Response>): Promise<Response> {\n if (worker.request.method !== GET) {\n return next();\n }\n\n if (!this.isMatch(worker.request)) {\n return next();\n }\n\n const headers = worker.request.headers;\n if (!hasConnectionHeader(headers)) {\n return new BadRequest(\"Missing or invalid 'Connection' header\").response();\n }\n if (!hasUpgradeHeader(headers)) {\n return new BadRequest(\"Missing or invalid 'Upgrade' header\").response();\n }\n if (!hasWebSocketVersion(headers)) {\n return new UpgradeRequired().response();\n }\n\n return next();\n }\n\n /**\n * Checks if the request path matches the configured path for this handler.\n *\n * @param request - The incoming Request object.\n * @returns `true` if the request path matches, `false` otherwise.\n */\n private isMatch(request: Request): boolean {\n return match(this.path)(new URL(request.url).pathname) !== false;\n }\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Middleware } from \"../../interfaces/middleware\";\n\nimport { WebSocketHandler } from \"./handler\";\n\n/**\n * Returns a middleware that validates incoming WebSocket upgrade requests.\n *\n * - Only validates the upgrade request; it does **not** perform the actual WebSocket upgrade.\n * - Ensures the request:\n * - Uses the `GET` method.\n * - Matches the specified path, supporting `path-to-regexp` style patterns\n * (e.g., `/chat/:name`).\n * - Contains required WebSocket headers:\n * - `Connection: Upgrade`\n * - `Upgrade: websocket`\n * - `Sec-WebSocket-Version: 13`\n * - Returns an error response if validation fails, otherwise passes control to\n * the next middleware or origin handler.\n *\n * @param path - The URL path to intercept for WebSocket upgrades. Defaults to `/`.\n * Supports dynamic segments using `path-to-regexp` syntax.\n * @returns A {@link Middleware} instance that can be used in your middleware chain.\n *\n * @example\n * ```ts\n * app.use(websocket(\"/chat/:name\"));\n * ```\n */\nexport function websocket(path: string = \"/\"): Middleware {\n return new WebSocketHandler(path);\n}\n"]}
1
+ {"version":3,"sources":["../src/constants/methods.ts","../src/constants/cache.ts","../src/constants/headers.ts","../src/middleware/websocket/constants.ts","../src/constants/media.ts","../src/utils/compare.ts","../src/utils/headers.ts","../src/utils/media.ts","../src/responses.ts","../src/errors.ts","../src/middleware/websocket/utils.ts","../src/middleware/websocket/handler.ts","../src/middleware/websocket/websocket.ts"],"names":["Method","GET","PUT","HEAD","POST","PATCH","DELETE","OPTIONS","CacheControl","CacheLib","HttpHeader","FORBIDDEN_304_HEADERS","FORBIDDEN_204_HEADERS","WS_UPGRADE","WS_WEBSOCKET","UTF8_CHARSET","lexCompare","a","b","setHeader","headers","key","value","raw","values","v","mergeHeader","merged","getHeaderValues","filterHeaders","keys","withCharset","mediaType","charset","BaseResponse","StatusCodes","getReasonPhrase","CacheResponse","cache","WorkerResponse","body","SuccessResponse","status","JsonResponse","json","HttpError","details","BadRequest","UpgradeRequired","hasConnectionHeader","hasUpgradeHeader","hasWebSocketVersion","WebSocketHandler","path","worker","next","request","match","websocket"],"mappings":"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","file":"websocket.js","sourcesContent":["/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Standard HTTP request methods.\n */\nexport enum Method {\n GET = \"GET\",\n PUT = \"PUT\",\n HEAD = \"HEAD\",\n POST = \"POST\",\n PATCH = \"PATCH\",\n DELETE = \"DELETE\",\n OPTIONS = \"OPTIONS\",\n}\n\n/**\n * Shorthand constants for each HTTP method.\n *\n * These are equivalent to the corresponding enum members in `Method`.\n * For example, `GET === Method.GET`.\n */\nexport const { GET, PUT, HEAD, POST, PATCH, DELETE, OPTIONS } = Method;\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport CacheLib from \"cache-control-parser\";\n\n/**\n * @see {@link https://github.com/etienne-martin/cache-control-parser | cache-control-parser}\n */\nexport type CacheControl = CacheLib.CacheControl;\nexport const CacheControl = {\n parse: CacheLib.parse,\n stringify: CacheLib.stringify,\n\n /** A CacheControl directive that disables all caching. */\n DISABLE: Object.freeze({\n \"no-cache\": true,\n \"no-store\": true,\n \"must-revalidate\": true,\n \"max-age\": 0,\n }) satisfies CacheControl,\n};\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Internally used headers.\n */\nexport const HttpHeader = {\n ACCEPT: \"accept\",\n ACCEPT_ENCODING: \"accept-encoding\",\n ACCEPT_LANGUAGE: \"accept-language\",\n ACCEPT_RANGES: \"accept-ranges\",\n ALLOW: \"allow\",\n AUTHORIZATION: \"authorization\",\n CACHE_CONTROL: \"cache-control\",\n CONNECTION: \"connection\",\n CONTENT_DISPOSITION: \"content-disposition\",\n CONTENT_ENCODING: \"content-encoding\",\n CONTENT_LANGUAGE: \"content-language\",\n CONTENT_LENGTH: \"content-length\",\n CONTENT_RANGE: \"content-range\",\n CONTENT_TYPE: \"content-type\",\n CONTENT_MD5: \"content-md5\",\n COOKIE: \"cookie\",\n ETAG: \"etag\",\n IF_MATCH: \"if-match\",\n IF_MODIFIED_SINCE: \"if-modified-since\",\n IF_NONE_MATCH: \"if-none-match\",\n IF_UNMODIFIED_SINCE: \"if-unmodified-since\",\n LAST_MODIFIED: \"last-modified\",\n ORIGIN: \"origin\",\n RANGE: \"range\",\n SET_COOKIE: \"set-cookie\",\n VARY: \"vary\",\n\n // Cors Headers\n ACCESS_CONTROL_ALLOW_CREDENTIALS: \"access-control-allow-credentials\",\n ACCESS_CONTROL_ALLOW_HEADERS: \"access-control-allow-headers\",\n ACCESS_CONTROL_ALLOW_METHODS: \"access-control-allow-methods\",\n ACCESS_CONTROL_ALLOW_ORIGIN: \"access-control-allow-origin\",\n ACCESS_CONTROL_EXPOSE_HEADERS: \"access-control-expose-headers\",\n ACCESS_CONTROL_MAX_AGE: \"access-control-max-age\",\n\n // Websocket Headers\n SEC_WEBSOCKET_VERSION: \"sec-websocket-version\",\n UPGRADE: \"upgrade\",\n\n // Internal Headers\n INTERNAL_VARIANT_SET: \"internal-variant-set\",\n} as const;\n\n/**\n * Headers that must not be sent in 304 Not Modified responses.\n * These are stripped to comply with the HTTP spec.\n */\nexport const FORBIDDEN_304_HEADERS = [\n HttpHeader.CONTENT_TYPE,\n HttpHeader.CONTENT_LENGTH,\n HttpHeader.CONTENT_RANGE,\n HttpHeader.CONTENT_ENCODING,\n HttpHeader.CONTENT_LANGUAGE,\n HttpHeader.CONTENT_DISPOSITION,\n HttpHeader.CONTENT_MD5,\n];\n\n/**\n * Headers that should not be sent in 204 No Content responses.\n * Stripping them is recommended but optional per spec.\n */\nexport const FORBIDDEN_204_HEADERS = [HttpHeader.CONTENT_LENGTH, HttpHeader.CONTENT_RANGE];\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/** WebSocket upgrade header value */\nexport const WS_UPGRADE = \"upgrade\";\n\n/** WebSocket protocol header value */\nexport const WS_WEBSOCKET = \"websocket\";\n\n/** WebSocket protocol version */\nexport const WS_VERSION = \"13\";\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const UTF8_CHARSET = \"utf-8\";\n\n/**\n * Internal media types.\n */\nexport enum MediaType {\n PLAIN_TEXT = \"text/plain\",\n HTML = \"text/html\",\n JSON = \"application/json\",\n OCTET_STREAM = \"application/octet-stream\",\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Lexicographically compares two strings.\n *\n * This comparator can be used in `Array.prototype.sort()` to produce a\n * consistent, stable ordering of string arrays.\n *\n * @param a - The first string to compare.\n * @param b - The second string to compare.\n * @returns A number indicating the relative order of `a` and `b`.\n */\nexport function lexCompare(a: string, b: string): number {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { lexCompare } from \"./compare\";\n\n/**\n * Sets a header on the given Headers object.\n *\n * - If `value` is an array, any duplicates and empty strings are removed.\n * - If the resulting value is empty, the header is deleted.\n * - Otherwise, values are joined with `\", \"` and set as the header value.\n *\n * @param headers - The Headers object to modify.\n * @param key - The header name to set.\n * @param value - The header value(s) to set. Can be a string or array of strings.\n */\nexport function setHeader(headers: Headers, key: string, value: string | string[]): void {\n const raw = Array.isArray(value) ? value : [value];\n const values = Array.from(new Set(raw.map((v) => v.trim())))\n .filter((v) => v.length)\n .sort(lexCompare);\n\n if (!values.length) {\n headers.delete(key);\n return;\n }\n\n headers.set(key, values.join(\", \"));\n}\n\n/**\n * Merges new value(s) into an existing header on the given Headers object.\n *\n * - Preserves any existing values and adds new ones.\n * - Removes duplicates and trims all values.\n * - If the header does not exist, it is created.\n * - If the resulting value array is empty, the header is deleted.\n *\n * @param headers - The Headers object to modify.\n * @param key - The header name to merge into.\n * @param value - The new header value(s) to add. Can be a string or array of strings.\n */\nexport function mergeHeader(headers: Headers, key: string, value: string | string[]): void {\n const values = Array.isArray(value) ? value : [value];\n if (values.length === 0) return;\n\n const existing = getHeaderValues(headers, key);\n const merged = existing.concat(values.map((v) => v.trim()));\n\n setHeader(headers, key, merged);\n}\n\n/**\n * Returns the values of an HTTP header as an array of strings.\n *\n * This helper:\n * - Retrieves the header value by `key`.\n * - Splits the value on commas.\n * - Trims surrounding whitespace from each entry.\n * - Filters out any empty tokens.\n * - Removes duplicate values (case-sensitive)\n *\n * If the header is not present, an empty array is returned.\n *\n */\nexport function getHeaderValues(headers: Headers, key: string): string[] {\n const values =\n headers\n .get(key)\n ?.split(\",\")\n .map((v) => v.trim())\n .filter((v) => v.length > 0) ?? [];\n return Array.from(new Set(values)).sort(lexCompare);\n}\n\n/**\n * Removes a list of header fields from a {@link Headers} object.\n *\n * @param headers - The {@link Headers} object to modify in place.\n * @param keys - An array of header field names to remove. Header names are\n * matched case-insensitively per the Fetch spec.\n */\nexport function filterHeaders(headers: Headers, keys: string[]): void {\n for (const key of keys) {\n headers.delete(key);\n }\n}\n\n/**\n * Extracts all header names from a `Headers` object, normalizes them,\n * and returns them in a stable, lexicographically sorted array.\n *\n * @param headers - The `Headers` object to extract keys from.\n * @returns A sorted array of lowercase header names.\n */\nexport function getHeaderKeys(headers: Headers): string[] {\n return [...headers.keys()].sort(lexCompare);\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Appends a charset parameter to a given media type string,\n * avoiding duplicates and ignoring empty charsets.\n *\n * @param {string} mediaType - The MIME type (e.g., \"text/html\").\n * @param {string} charset - The character set to append (e.g., \"utf-8\").\n * @returns {string} The media type with charset appended if provided.\n */\nexport function withCharset(mediaType: string, charset: string): string {\n if (!charset || mediaType.toLowerCase().includes(\"charset=\")) {\n return mediaType;\n }\n return `${mediaType}; charset=${charset.toLowerCase()}`;\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getReasonPhrase } from \"http-status-codes/build/es/utils-functions\";\n\nimport { StatusCodes } from \"./constants\";\nimport { CacheControl } from \"./constants/cache\";\nimport { FORBIDDEN_204_HEADERS, FORBIDDEN_304_HEADERS, HttpHeader } from \"./constants/headers\";\nimport { MediaType, UTF8_CHARSET } from \"./constants/media\";\nimport { GET, HEAD } from \"./constants/methods\";\nimport { assertMethods } from \"./guards/methods\";\nimport { assertOctetStreamInit } from \"./guards/responses\";\nimport { Worker } from \"./interfaces\";\nimport { OctetStreamInit } from \"./interfaces/response\";\nimport { filterHeaders, mergeHeader, setHeader } from \"./utils/headers\";\nimport { withCharset } from \"./utils/media\";\n\n/**\n * Base class for building HTTP responses.\n * Manages headers, status, and media type.\n */\nabstract class BaseResponse {\n /** HTTP headers for the response. */\n public headers: Headers = new Headers();\n\n /** HTTP status code (default 200 OK). */\n public status: StatusCodes = StatusCodes.OK;\n\n /** Optional status text. Defaults to standard reason phrase. */\n public statusText?: string;\n\n /** Optional websocket property. */\n public webSocket?: WebSocket | null;\n\n /** Default media type of the response body. */\n public mediaType: string = withCharset(MediaType.PLAIN_TEXT, UTF8_CHARSET);\n\n /** Converts current state to ResponseInit for constructing a Response. */\n protected get responseInit(): ResponseInit {\n return {\n headers: this.headers,\n status: this.status,\n statusText: this.statusText ?? getReasonPhrase(this.status),\n webSocket: this.webSocket,\n encodeBody: \"automatic\",\n };\n }\n\n /** Sets a header, overwriting any existing value. */\n public setHeader(key: string, value: string | string[]): void {\n setHeader(this.headers, key, value);\n }\n\n /** Merges a header with existing values (does not overwrite). */\n public mergeHeader(key: string, value: string | string[]): void {\n mergeHeader(this.headers, key, value);\n }\n\n /** Adds a Content-Type header if not already existing (does not overwrite). */\n public addContentType() {\n if (!this.headers.get(HttpHeader.CONTENT_TYPE)) {\n this.setHeader(HttpHeader.CONTENT_TYPE, this.mediaType);\n }\n }\n\n /**\n * Removes headers that are disallowed or discouraged based on the current\n * status code.\n *\n * - **204 No Content:** strips headers that \"should not\" be sent\n * (`Content-Length`, `Content-Range`), per the HTTP spec.\n * - **304 Not Modified:** strips headers that \"must not\" be sent\n * (`Content-Type`, `Content-Length`, `Content-Range`, etc.), per the HTTP spec.\n *\n * This ensures that responses remain compliant with HTTP/1.1 standards while preserving\n * custom headers that are allowed.\n */\n public filterHeaders(): void {\n if (this.status === StatusCodes.NO_CONTENT) {\n filterHeaders(this.headers, FORBIDDEN_204_HEADERS);\n } else if (this.status === StatusCodes.NOT_MODIFIED) {\n filterHeaders(this.headers, FORBIDDEN_304_HEADERS);\n }\n }\n}\n\n/**\n * Base response class that adds caching headers.\n */\nabstract class CacheResponse extends BaseResponse {\n constructor(public cache?: CacheControl) {\n super();\n }\n\n /** Adds Cache-Control header if caching is configured. */\n protected addCacheHeader(): void {\n if (this.cache) {\n this.setHeader(HttpHeader.CACHE_CONTROL, CacheControl.stringify(this.cache));\n }\n }\n}\n\n/**\n * Core response. Combines caching, and content type headers.\n */\nexport abstract class WorkerResponse extends CacheResponse {\n constructor(\n private readonly body: BodyInit | null = null,\n cache?: CacheControl,\n ) {\n super(cache);\n }\n\n /** Builds the Response with body, headers, and status. */\n public async response(): Promise<Response> {\n this.addCacheHeader();\n\n const body = [StatusCodes.NO_CONTENT, StatusCodes.NOT_MODIFIED].includes(this.status)\n ? null\n : this.body;\n\n if (body) this.addContentType();\n\n this.filterHeaders();\n\n return new Response(body, this.responseInit);\n }\n}\n\n/**\n * Copies an existing response for mutation. Pass in a CacheControl\n * to be used for the response, overriding any existing `cache-control`\n * on the source response.\n */\nexport class CopyResponse extends WorkerResponse {\n constructor(response: Response, cache?: CacheControl) {\n super(response.body, cache);\n this.status = response.status;\n this.statusText = response.statusText;\n this.headers = new Headers(response.headers);\n }\n}\n\n/**\n * Copies the response, but with null body and status 304 Not Modified.\n */\nexport class NotModified extends WorkerResponse {\n constructor(response: Response) {\n super();\n this.status = StatusCodes.NOT_MODIFIED;\n this.headers = new Headers(response.headers);\n }\n}\n\n/**\n * Represents a successful response with customizable body, cache and status.\n */\nexport class SuccessResponse extends WorkerResponse {\n constructor(\n body: BodyInit | null = null,\n cache?: CacheControl,\n status: StatusCodes = StatusCodes.OK,\n ) {\n super(body, cache);\n this.status = status;\n }\n}\n\n/**\n * JSON response. Automatically sets Content-Type to application/json.\n */\nexport class JsonResponse extends SuccessResponse {\n constructor(json: unknown = {}, cache?: CacheControl, status: StatusCodes = StatusCodes.OK) {\n super(JSON.stringify(json), cache, status);\n this.mediaType = withCharset(MediaType.JSON, UTF8_CHARSET);\n }\n}\n\n/**\n * HTML response. Automatically sets Content-Type to text/html.\n */\nexport class HtmlResponse extends SuccessResponse {\n constructor(\n body: string,\n cache?: CacheControl,\n status: StatusCodes = StatusCodes.OK,\n charset: string = UTF8_CHARSET,\n ) {\n super(body, cache, status);\n this.mediaType = withCharset(MediaType.HTML, charset);\n }\n}\n\n/**\n * Plain text response. Automatically sets Content-Type to text/plain.\n */\nexport class TextResponse extends SuccessResponse {\n constructor(\n body: string,\n cache?: CacheControl,\n status: StatusCodes = StatusCodes.OK,\n charset: string = UTF8_CHARSET,\n ) {\n super(body, cache, status);\n this.mediaType = withCharset(MediaType.PLAIN_TEXT, charset);\n }\n}\n\n/**\n * Represents an HTTP response for serving binary data as `application/octet-stream`.\n *\n * This class wraps a `ReadableStream` and sets all necessary headers for both\n * full and partial content responses, handling range requests in a hybrid way\n * to maximize browser and CDN caching.\n *\n * Key behaviors:\n * - `Content-Type` is set to `application/octet-stream`.\n * - `Accept-Ranges: bytes` is always included.\n * - `Content-Length` is always set to the validated length of the response body.\n * - If the request is a true partial range (offset > 0 or length < size), the response\n * will be `206 Partial Content` with the appropriate `Content-Range` header.\n * - If the requested range covers the entire file (even if a Range header is present),\n * the response will return `200 OK` to enable browser and edge caching.\n * - Zero-length streams (`size = 0`) are never treated as partial.\n * - Special case: a requested range of `0-0` on a non-empty file is normalized to 1 byte.\n */\nexport class OctetStream extends WorkerResponse {\n constructor(stream: ReadableStream, init: OctetStreamInit, cache?: CacheControl) {\n assertOctetStreamInit(init);\n\n super(stream, cache);\n this.mediaType = MediaType.OCTET_STREAM;\n\n const normalized = OctetStream.normalizeInit(init);\n const { size, offset, length } = normalized;\n\n if (OctetStream.isPartial(normalized)) {\n this.setHeader(\n HttpHeader.CONTENT_RANGE,\n `bytes ${offset}-${offset + length - 1}/${size}`,\n );\n this.status = StatusCodes.PARTIAL_CONTENT;\n }\n\n this.setHeader(HttpHeader.ACCEPT_RANGES, \"bytes\");\n this.setHeader(HttpHeader.CONTENT_LENGTH, `${length}`);\n }\n\n /**\n * Normalizes a partially-specified `OctetStreamInit` into a fully-specified object.\n *\n * Ensures that all required fields (`size`, `offset`, `length`) are defined:\n * - `offset` defaults to 0 if not provided.\n * - `length` defaults to `size - offset` if not provided.\n * - Special case: if `offset` and `length` are both 0 but `size > 0`, `length` is set to 1\n * to avoid zero-length partial streams.\n *\n * @param init - The initial `OctetStreamInit` object, possibly with missing `offset` or `length`.\n * @returns A fully-specified `OctetStreamInit` object with `size`, `offset`, and `length` guaranteed.\n */\n private static normalizeInit(init: OctetStreamInit): Required<OctetStreamInit> {\n const { size } = init;\n const offset = init.offset ?? 0;\n let length = init.length ?? size - offset;\n\n if (offset === 0 && length === 0 && size > 0) {\n length = 1;\n }\n\n return { size, offset, length };\n }\n\n /**\n * Determines whether the given `OctetStreamInit` represents a partial range.\n *\n * Partial ranges are defined as any range that does **not** cover the entire file:\n * - If `size === 0`, the stream is never partial.\n * - If `offset === 0` and `length === size`, the stream is treated as a full file (not partial),\n * even if a Range header is present. This enables browser and CDN caching.\n * - All other cases are considered partial, and will result in a `206 Partial Content` response.\n *\n * @param init - A fully-normalized `OctetStreamInit` object.\n * @returns `true` if the stream represents a partial range; `false` if it represents the full file.\n */\n private static isPartial(init: Required<OctetStreamInit>): boolean {\n if (init.size === 0) return false;\n return !(init.offset === 0 && init.length === init.size);\n }\n}\n\n/**\n * A streaming response for Cloudflare R2 objects.\n *\n * **Partial content support:** To enable HTTP 206 streaming, you must provide\n * request headers containing the `Range` header when calling the R2 bucket's `get()` method.\n *\n * Example:\n * ```ts\n * const stream = await this.env.R2_BUCKET.get(\"key\", { range: this.request.headers });\n * ```\n *\n * @param source - The R2 object to stream.\n * @param cache - Optional caching override.\n */\nexport class R2ObjectStream extends OctetStream {\n constructor(source: R2ObjectBody, cache?: CacheControl) {\n let useCache = cache;\n if (!useCache && source.httpMetadata?.cacheControl) {\n useCache = CacheControl.parse(source.httpMetadata.cacheControl);\n }\n\n super(source.body, R2ObjectStream.computeRange(source.size, source.range), useCache);\n\n this.setHeader(HttpHeader.ETAG, source.httpEtag);\n\n if (source.httpMetadata?.contentType) {\n this.mediaType = source.httpMetadata.contentType;\n }\n }\n\n /**\n * Computes an `OctetStreamInit` object from a given R2 range.\n *\n * This function normalizes a Cloudflare R2 `R2Range` into the shape expected\n * by `OctetStream`. It handles the following cases:\n *\n * - No range provided: returns `{ size }` (full content).\n * - `suffix` range: calculates the offset and length from the end of the file.\n * - Explicit `offset` and/or `length`: passed through as-is.\n *\n * @param size - The total size of the file/object.\n * @param range - Optional range to extract (from R2). Can be:\n * - `{ offset: number; length?: number }`\n * - `{ offset?: number; length: number }`\n * - `{ suffix: number }`\n * @returns An `OctetStreamInit` object suitable for `OctetStream`.\n */\n private static computeRange(size: number, range?: R2Range): OctetStreamInit {\n if (!range) return { size };\n\n if (\"suffix\" in range) {\n const offset = Math.max(0, size - range.suffix);\n const length = size - offset;\n return { size, offset, length };\n }\n\n return { size, ...range };\n }\n}\n\n/**\n * Response for WebSocket upgrade requests.\n * Automatically sets status to 101 and attaches the client socket.\n */\nexport class WebSocketUpgrade extends WorkerResponse {\n constructor(client: WebSocket) {\n super();\n this.status = StatusCodes.SWITCHING_PROTOCOLS;\n this.webSocket = client;\n }\n}\n\n/**\n * Response for `HEAD` requests. Copy headers and status from a `GET` response\n * without the body.\n */\nexport class Head extends WorkerResponse {\n constructor(get: Response) {\n super();\n this.status = get.status;\n this.statusText = get.statusText;\n this.headers = new Headers(get.headers);\n }\n}\n\n/**\n * Response for `OPTIONS` requests.\n */\nexport class Options extends WorkerResponse {\n constructor(worker: Worker) {\n const allowed = Array.from(new Set([GET, HEAD, ...worker.getAllowedMethods()]));\n assertMethods(allowed);\n\n super();\n this.status = StatusCodes.NO_CONTENT;\n this.setHeader(HttpHeader.ALLOW, allowed);\n }\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { getReasonPhrase } from \"http-status-codes/build/es/utils-functions\";\n\nimport { StatusCodes } from \"./constants\";\nimport { CacheControl } from \"./constants/cache\";\nimport { HttpHeader } from \"./constants/headers\";\nimport { assertMethods } from \"./guards/methods\";\nimport { ErrorJson } from \"./interfaces/error\";\nimport { Worker } from \"./interfaces/worker\";\nimport { WS_VERSION } from \"./middleware/websocket/constants\";\nimport { JsonResponse } from \"./responses\";\n\n/**\n * Generic HTTP error response.\n * Sends a JSON body with status, error message, and details.\n */\nexport class HttpError extends JsonResponse {\n /**\n * @param worker The worker handling the request.\n * @param status HTTP status code.\n * @param details Optional detailed error message.\n */\n constructor(\n status: StatusCodes,\n protected readonly details?: string,\n ) {\n const json: ErrorJson = {\n status,\n error: getReasonPhrase(status),\n details: details ?? \"\",\n };\n super(json, CacheControl.DISABLE, status);\n }\n}\n\n/**\n * Creates a structured error response without exposing the error\n * details to the client. Links the sent response to the logged\n * error via a generated correlation ID.\n *\n * Status defaults to 500 Internal Server Error.\n */\nexport class LoggedHttpError extends HttpError {\n constructor(error: unknown, status: StatusCodes = StatusCodes.INTERNAL_SERVER_ERROR) {\n const uuid = crypto.randomUUID();\n console.error(uuid, error);\n super(status, uuid);\n }\n}\n\n/** 400 Bad Request error response. */\nexport class BadRequest extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.BAD_REQUEST, details);\n }\n}\n\n/** 401 Unauthorized error response. */\nexport class Unauthorized extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.UNAUTHORIZED, details);\n }\n}\n\n/** 403 Forbidden error response. */\nexport class Forbidden extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.FORBIDDEN, details);\n }\n}\n\n/** 404 Not Found error response. */\nexport class NotFound extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.NOT_FOUND, details);\n }\n}\n\n/** 405 Method Not Allowed error response. */\nexport class MethodNotAllowed extends HttpError {\n constructor(worker: Worker) {\n const methods = worker.getAllowedMethods();\n assertMethods(methods);\n\n super(StatusCodes.METHOD_NOT_ALLOWED, `${worker.request.method} method not allowed.`);\n this.setHeader(HttpHeader.ALLOW, methods);\n }\n}\n\n/** 412 Precondition Failed error response */\nexport class PreconditionFailed extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.PRECONDITION_FAILED, details);\n }\n}\n\n/** 426 Upgrade Required error response. */\nexport class UpgradeRequired extends HttpError {\n constructor() {\n super(StatusCodes.UPGRADE_REQUIRED);\n this.setHeader(HttpHeader.SEC_WEBSOCKET_VERSION, WS_VERSION);\n }\n}\n\n/** 500 Internal Server Error response. */\nexport class InternalServerError extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.INTERNAL_SERVER_ERROR, details);\n }\n}\n\n/** 501 Not Implemented error response. */\nexport class NotImplemented extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.NOT_IMPLEMENTED, details);\n }\n}\n\n/** 501 Method Not Implemented error response for unsupported HTTP methods. */\nexport class MethodNotImplemented extends NotImplemented {\n constructor(worker: Worker) {\n super(`${worker.request.method} method not implemented.`);\n }\n}\n\n/** 503 Service Unavailable error response. */\nexport class ServiceUnavailable extends HttpError {\n constructor(details?: string) {\n super(StatusCodes.SERVICE_UNAVAILABLE, details);\n }\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { HttpHeader } from \"../../constants/headers\";\nimport { getHeaderValues } from \"../../utils/headers\";\n\nimport { WS_UPGRADE, WS_VERSION, WS_WEBSOCKET } from \"./constants\";\n\n/**\n * Checks if the `Connection` header includes the WebSocket upgrade token.\n *\n * @param headers - The Headers object to inspect.\n * @returns `true` if a WebSocket upgrade is requested via `Connection` header, `false` otherwise.\n */\nexport function hasConnectionHeader(headers: Headers): boolean {\n return getHeaderValues(headers, HttpHeader.CONNECTION).some(\n (value) => value.toLowerCase() === WS_UPGRADE,\n );\n}\n\n/**\n * Checks if the `Upgrade` header requests a WebSocket upgrade.\n *\n * @param headers - The Headers object to inspect.\n * @returns `true` if the `Upgrade` header is set to `websocket`, `false` otherwise.\n */\nexport function hasUpgradeHeader(headers: Headers): boolean {\n return getHeaderValues(headers, HttpHeader.UPGRADE).some(\n (value) => value.toLowerCase() === WS_WEBSOCKET,\n );\n}\n\n/**\n * Determines whether the request includes the correct WebSocket version.\n *\n * @param headers - The Headers object to inspect.\n * @returns `true` if `Sec-WebSocket-Version` matches the expected version, `false` otherwise.\n */\nexport function hasWebSocketVersion(headers: Headers): boolean {\n return headers.get(HttpHeader.SEC_WEBSOCKET_VERSION)?.trim() === WS_VERSION;\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { match } from \"path-to-regexp\";\n\nimport { GET } from \"../../constants/methods\";\nimport { BadRequest, UpgradeRequired } from \"../../errors\";\nimport { Middleware } from \"../../interfaces/middleware\";\nimport { Worker } from \"../../interfaces/worker\";\n\nimport { hasConnectionHeader, hasUpgradeHeader, hasWebSocketVersion } from \"./utils\";\n\n/**\n * Middleware for validating WebSocket upgrade requests.\n *\n * - Only applies to `GET` requests.\n * - Matches requests against a specific path using `path-to-regexp` patterns.\n * - Validates that the request contains required WebSocket headers:\n * - `Connection: Upgrade`\n * - `Upgrade: websocket`\n * - `Sec-WebSocket-Version` matches the expected version\n * - Returns an error response if any validation fails.\n * - Otherwise, passes control to the next middleware or origin handler.\n */\nexport class WebSocketHandler implements Middleware {\n /**\n * Creates a new WebSocketHandler for a specific path.\n *\n * @param path - The request path this handler should intercept for WebSocket upgrades.\n * Supports dynamic segments using `path-to-regexp` syntax.\n */\n constructor(private readonly path: string) {}\n\n /**\n * Handles an incoming request, validating WebSocket upgrade headers.\n *\n * @param worker - The Worker instance containing the request.\n * @param next - Function to invoke the next middleware.\n * @returns A Response object if the request fails WebSocket validation,\n * or the result of `next()` if the request is valid or does not match.\n */\n public handle(worker: Worker, next: () => Promise<Response>): Promise<Response> {\n if (worker.request.method !== GET) {\n return next();\n }\n\n if (!this.isMatch(worker.request)) {\n return next();\n }\n\n const headers = worker.request.headers;\n if (!hasConnectionHeader(headers)) {\n return new BadRequest(\"Missing or invalid 'Connection' header\").response();\n }\n if (!hasUpgradeHeader(headers)) {\n return new BadRequest(\"Missing or invalid 'Upgrade' header\").response();\n }\n if (!hasWebSocketVersion(headers)) {\n return new UpgradeRequired().response();\n }\n\n return next();\n }\n\n /**\n * Checks if the request path matches the configured path for this handler.\n *\n * @param request - The incoming Request object.\n * @returns `true` if the request path matches, `false` otherwise.\n */\n private isMatch(request: Request): boolean {\n return match(this.path)(new URL(request.url).pathname) !== false;\n }\n}\n","/*\n * Copyright (C) 2025 Ty Busby\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Middleware } from \"../../interfaces/middleware\";\n\nimport { WebSocketHandler } from \"./handler\";\n\n/**\n * Returns a middleware that validates incoming WebSocket upgrade requests.\n *\n * - Only validates the upgrade request; it does **not** perform the actual WebSocket upgrade.\n * - Ensures the request:\n * - Uses the `GET` method.\n * - Matches the specified path, supporting `path-to-regexp` style patterns\n * (e.g., `/chat/:name`).\n * - Contains required WebSocket headers:\n * - `Connection: Upgrade`\n * - `Upgrade: websocket`\n * - `Sec-WebSocket-Version: 13`\n * - Returns an error response if validation fails, otherwise passes control to\n * the next middleware or origin handler.\n *\n * @param path - The URL path to intercept for WebSocket upgrades. Defaults to `/`.\n * Supports dynamic segments using `path-to-regexp` syntax.\n * @returns A {@link Middleware} instance that can be used in your middleware chain.\n *\n * @example\n * ```ts\n * app.use(websocket(\"/chat/:name\"));\n * ```\n */\nexport function websocket(path: string = \"/\"): Middleware {\n return new WebSocketHandler(path);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonix.org/cloud-spark",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Ignite your Cloudflare Workers with a type-safe library for rapid development.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -13,19 +13,19 @@
13
13
  },
14
14
  "./cache": {
15
15
  "import": "./dist/cache.js",
16
- "types": "./dist/index.d.ts"
16
+ "types": "./dist/cache.d.ts"
17
17
  },
18
18
  "./cors": {
19
19
  "import": "./dist/cors.js",
20
- "types": "./dist/index.d.ts"
20
+ "types": "./dist/cors.d.ts"
21
21
  },
22
22
  "./websocket": {
23
23
  "import": "./dist/websocket.js",
24
- "types": "./dist/index.d.ts"
24
+ "types": "./dist/websocket.d.ts"
25
25
  },
26
26
  "./sessions": {
27
27
  "import": "./dist/sessions.js",
28
- "types": "./dist/index.d.ts"
28
+ "types": "./dist/sessions.d.ts"
29
29
  }
30
30
  },
31
31
  "files": [
@@ -68,9 +68,9 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@eslint/js": "^9.39.1",
71
- "@types/node": "^24.10.1",
72
- "@typescript-eslint/eslint-plugin": "^8.48.1",
73
- "@typescript-eslint/parser": "^8.48.1",
71
+ "@types/node": "^25.0.0",
72
+ "@typescript-eslint/eslint-plugin": "^8.49.0",
73
+ "@typescript-eslint/parser": "^8.49.0",
74
74
  "@vitest/coverage-v8": "^4.0.15",
75
75
  "eslint": "^9.39.1",
76
76
  "eslint-plugin-import": "^2.32.0",
@@ -79,9 +79,9 @@
79
79
  "prettier": "^3.7.4",
80
80
  "tsup": "^8.5.1",
81
81
  "typescript": "^5.9.3",
82
- "typescript-eslint": "^8.48.1",
82
+ "typescript-eslint": "^8.49.0",
83
83
  "vitest": "^4.0.15",
84
- "wrangler": "^4.53.0"
84
+ "wrangler": "^4.54.0"
85
85
  },
86
86
  "dependencies": {
87
87
  "cache-control-parser": "^2.0.6",