Add gradle plugin and fix authenticator

This commit is contained in:
2026-01-17 23:23:46 +08:00
parent 94a486a505
commit 546977fe86
40 files changed
+266 -86

No files matched your search

@@ -0,0 +1,39 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 17:29
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker
import org.w3c.fetch.ResponseInit
/**
* Auto add cors response headers
*/
@Suppress("FunctionName")
internal fun WorkerApplication._addCorsHeaders(
init: ResponseInit,
origin: String = "*",
allowedMethods: Set<HttpMethod> = setOf(
HttpMethod.GET, HttpMethod.POST,
HttpMethod.DELETE, HttpMethod.DELETE,
HttpMethod.OPTIONS
),
): ResponseInit {
val headers = if (this.corsConfig.enabled) {
val corsHeaders = init.headers ?: js("{}")
corsHeaders["Access-Control-Allow-Origin"] = origin
corsHeaders["Access-Control-Allow-Methods"] = allowedMethods.joinToString(", ")
corsHeaders["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
corsHeaders["Access-Control-Max-Age"] = "86400"
corsHeaders
} else init.headers
return ResponseInit(
headers = headers,
status = init.status,
statusText = init.statusText
)
}
@@ -0,0 +1,38 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 01:02
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker
import org.w3c.fetch.Request
/**
* Common http method
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods
*/
public enum class HttpMethod {
GET,
POST,
PUT,
DELETE,
PATCH,
HEAD,
OPTIONS,
TRACE,
CONNECT;
public companion object {
public fun fromString(method: String): HttpMethod? =
entries.firstOrNull { it.name.equals(method, ignoreCase = true) }
}
}
/**
* Get parsed http method, if it's not a standard http method return GET
*/
public val Request.httpMethod: HttpMethod
get() = HttpMethod.fromString(this.method) ?: HttpMethod.GET
@@ -0,0 +1,89 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 01:03
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker
import cn.rtast.cfworker.auth.AuthResult
import cn.rtast.cfworker.auth.packBasicCredential
import cn.rtast.cfworker.auth.packBearerCredential
import cn.rtast.cfworker.auth.provider.BasicAuthenticator
import cn.rtast.cfworker.auth.provider.BearerAuthenticator
import cn.rtast.cfworker.config.CORSConfig
import cn.rtast.cfworker.response.respondEmpty
import cn.rtast.cfworker.response.respondText
import cn.rtast.cfworker.route.type.AbstractRoute
import cn.rtast.cfworker.route.type.RegexRoute
import cn.rtast.cfworker.route.type.StringRoute
import cn.rtast.cfworker.util.decodeBase64String
import cn.rtast.cfworker.websocket.WebsocketEventHandler
import cn.rtast.cfworker.websocket.WebsocketRoute
import org.w3c.dom.url.URL
import org.w3c.fetch.Request
import org.w3c.fetch.Response
import org.w3c.fetch.ResponseInit
/**
* Kotlin cloudflare worker logic entrypoint class
*/
public class WorkerApplication(
public val corsConfig: CORSConfig = CORSConfig(),
) {
internal val routes: MutableList<AbstractRoute> = mutableListOf()
internal val websocketRoutes: MutableList<WebsocketRoute> = mutableListOf()
public suspend fun handle(request: Request): Response {
val url = URL(request.url)
val path = url.pathname
val method = HttpMethod.fromString(request.method) ?: HttpMethod.GET
if (method == HttpMethod.OPTIONS && corsConfig.enabled) return respondEmpty()
if (request.headers.get("upgrade") != null) {
val route = websocketRoutes.firstOrNull { r ->
r.stringPath?.let { it == url.pathname } ?: r.regexPath?.matches(url.pathname) ?: false
} ?: return Response("Not Found", ResponseInit(404))
val handler = WebsocketEventHandler(request = request)
route.block(handler)
return handler.handle()
}
val matchedRoutes = routes.filter {
when (it) {
is StringRoute -> it.path == path
is RegexRoute -> it.path.matches(path)
else -> false
}
}
if (matchedRoutes.isEmpty()) return respondText("Not Found", 404)
val methodMatchedRoute = matchedRoutes.firstOrNull { method in it.methods }
if (methodMatchedRoute == null) {
val allowMethods = matchedRoutes.flatMap { it.methods }.distinct().joinToString(", ")
val headers: dynamic = js("{}")
headers["Allow"] = allowMethods
return respondText("Method Not Allowed", 405)
}
val auth = methodMatchedRoute.authenticator
val responsePromise = if (auth != null) {
val credential = when (auth) {
is BasicAuthenticator -> request.headers.get("authorization")
?.removePrefix("Basic ")
?.decodeBase64String
?.packBasicCredential()
is BearerAuthenticator -> request.headers.get("authorization")
?.removePrefix("Bearer ")
?.packBearerCredential()
}
when (auth.authenticate(request, credential)) {
AuthResult.OK -> methodMatchedRoute.handle(request)
AuthResult.UNAUTHORIZED -> respondText("UNAUTHORIZED", 401)
AuthResult.FORBIDDEN -> respondText("FORBIDDEN", 403)
}
} else methodMatchedRoute.handle(request)
return responsePromise
}
}
@@ -0,0 +1,13 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/25 11:41
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker.auth
public enum class AuthResult {
OK, UNAUTHORIZED, FORBIDDEN
}
@@ -0,0 +1,18 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.auth
import cn.rtast.cfworker.auth.credentials.BasicCredential
import cn.rtast.cfworker.auth.credentials.BearerCredential
internal fun String.packBasicCredential(): BasicCredential {
val parts = this.split(":")
return BasicCredential(parts.first(), parts.last())
}
internal fun String.packBearerCredential(): BearerCredential = BearerCredential(this)
@@ -0,0 +1,16 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.auth.credentials
/**
* Basic credential
*/
public data class BasicCredential(
val username: String,
val password: String,
) : HttpCredential
@@ -0,0 +1,15 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.auth.credentials
/**
* Bearer credential
*/
public data class BearerCredential(
val token: String,
) : HttpCredential
@@ -0,0 +1,11 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.auth.credentials
@Suppress("CLASSNAME")
public sealed interface HttpCredential
@@ -0,0 +1,13 @@
package cn.rtast.cfworker.auth.provider
import cn.rtast.cfworker.auth.AuthResult
import cn.rtast.cfworker.auth.credentials.HttpCredential
import org.w3c.fetch.Request
/**
* An interface for authenticating
* It's sealed
*/
public sealed interface Authenticator<out C : HttpCredential> {
public suspend fun authenticate(request: Request, credential: @UnsafeVariance C?): AuthResult
}
@@ -0,0 +1,13 @@
package cn.rtast.cfworker.auth.provider
import cn.rtast.cfworker.auth.AuthResult
import cn.rtast.cfworker.auth.credentials.BasicCredential
import org.w3c.fetch.Request
/**
* Basic auth provider
* Schema: Basic <Base64-encoded username:password string>
*/
public fun interface BasicAuthenticator : Authenticator<BasicCredential> {
override suspend fun authenticate(request: Request, credential: BasicCredential?): AuthResult
}
@@ -0,0 +1,20 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.auth.provider
import cn.rtast.cfworker.auth.AuthResult
import cn.rtast.cfworker.auth.credentials.BearerCredential
import org.w3c.fetch.Request
/**
* Bearer token auth provider
* Schema: Bearer <String>
*/
public fun interface BearerAuthenticator : Authenticator<BearerCredential> {
override suspend fun authenticate(request: Request, credential: BearerCredential?): AuthResult
}
@@ -0,0 +1,35 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.client
import cn.rtast.cfworker.util.toByteArray
import kotlinx.coroutines.await
import org.w3c.dom.url.URL
import org.w3c.fetch.Request
import org.w3c.fetch.RequestInit
import org.w3c.fetch.Response
import kotlin.js.Promise
/**
* define fetch function externally
*/
public external fun fetch(
input: String,
init: RequestInit = definedExternally,
): Promise<Response>
/**
* Get request raw body, http POST
*/
public suspend fun Request.rawBody(): ByteArray =
this.arrayBuffer().await().toByteArray()
/**
* Get [URL] object
*/
public val Request.Url: URL get() = URL(this.url)
@@ -0,0 +1,31 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.config
import cn.rtast.cfworker.HttpMethod
public class CORSConfig {
/**
* enabled cors agent
*/
public var enabled: Boolean = true
/**
* Allowed origin
*/
public var origin: String = "*"
/**
* Allowed methods
*/
public var allowedMethods: Set<HttpMethod> = setOf(
HttpMethod.GET, HttpMethod.POST,
HttpMethod.DELETE, HttpMethod.DELETE,
HttpMethod.OPTIONS
)
}
@@ -0,0 +1,65 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
/**
* This file is for internally use
*/
@file:OptIn(DelicateCoroutinesApi::class, ExperimentalJsExport::class)
package cn.rtast.cfworker
import cn.rtast.cfworker.auth.AuthResult
import cn.rtast.cfworker.auth.provider.BearerAuthenticator
import cn.rtast.cfworker.client.Url
import cn.rtast.cfworker.response.respondText
import cn.rtast.cfworker.route.route
import cn.rtast.cfworker.websocket.readText
import cn.rtast.cfworker.websocket.webSocket
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
import org.w3c.dom.events.EventListener
import org.w3c.fetch.Request
import org.w3c.fetch.Response
import kotlin.js.Promise
public fun main() {
@Suppress("unused_expression")
val eventListener = EventListener { event ->
val dyn = event.asDynamic()
event.asDynamic().respondWith(handleRequest(dyn.request as Request))
Unit
}
js("addEventListener('fetch', eventListener)")
}
@JsExport
public fun handleRequest(request: Request): Promise<Response> = GlobalScope.promise {
val server = WorkerApplication().apply {
route("/") {
respondText("Hello kotlin cloudflare worker")
}
webSocket("/ws") {
onMessage {
println(it.readText())
}
onClose {
}
}
route("/protected", setOf(HttpMethod.GET), BearerAuthenticator { request, credential ->
val name = request.Url.searchParams.get("name")
return@BearerAuthenticator if (name == "admin") AuthResult.OK else AuthResult.UNAUTHORIZED
}) {
respondText("Success")
}
}
return@promise server.handle(request)
}
@@ -0,0 +1,87 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 01:17
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
@file:Suppress("unused")
package cn.rtast.cfworker.response
import cn.rtast.cfworker.WorkerApplication
import cn.rtast.cfworker._addCorsHeaders
import org.khronos.webgl.Uint8Array
import org.w3c.fetch.Response
import org.w3c.fetch.ResponseInit
public fun WorkerApplication.respondBytes(
bytes: ByteArray,
status: Int = 200,
contentType: String? = null,
headers: Map<String, String> = mapOf(),
): Response {
val respHeaders: dynamic = js("{}")
headers.forEach { respHeaders[it.key] = it.value }
respHeaders["Content-Type"] = contentType ?: "application/octet-stream"
return Response(
Uint8Array(bytes.toTypedArray()),
_addCorsHeaders(
ResponseInit(
status = status.toShort(),
headers = respHeaders
)
)
)
}
public fun WorkerApplication.respondStream(
stream: dynamic,
status: Int = 200,
headers: Map<String, String> = mapOf(),
): Response {
val respHeaders: dynamic = js("{}")
headers.forEach { respHeaders[it.key] = it.value }
return Response(
stream,
_addCorsHeaders(
ResponseInit(
status = status.toShort(),
headers = respHeaders
)
)
)
}
public fun WorkerApplication.respondEmpty(
status: Int = 204,
headers: Map<String, String> = mapOf(),
): Response {
val respHeaders: dynamic = js("{}")
headers.forEach { respHeaders[it.key] = it.value }
return Response(null, _addCorsHeaders(ResponseInit(status = status.toShort(), headers = respHeaders)))
}
public fun WorkerApplication.respondText(
content: String,
status: Int = 200,
headers: Map<String, String> = mapOf(),
): Response {
val respHeaders: dynamic = js("{}")
headers.forEach { respHeaders[it.key] = it.value }
return Response(content, _addCorsHeaders(ResponseInit(status = status.toShort(), headers = respHeaders)))
}
public fun WorkerApplication.respondRedirect(
location: String,
status: Int = 302,
headers: Map<String, String> = mapOf(),
): Response {
val respHeaders: dynamic = js("{}")
headers.forEach { respHeaders[it.key] = it.value }
respHeaders["Location"] = location
return Response(
"Redirecting to $location",
_addCorsHeaders(ResponseInit(status = status.toShort(), headers = respHeaders))
)
}
@@ -0,0 +1,14 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/25 11:43
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker.route
import org.w3c.fetch.Request
import org.w3c.fetch.Response
public typealias Handler = suspend (Request) -> Response
@@ -0,0 +1,44 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/25 10:49
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
@file:Suppress("unused")
package cn.rtast.cfworker.route
import cn.rtast.cfworker.HttpMethod
import cn.rtast.cfworker.WorkerApplication
import cn.rtast.cfworker.auth.credentials.HttpCredential
import cn.rtast.cfworker.auth.provider.Authenticator
import cn.rtast.cfworker.route.type.RegexRoute
import cn.rtast.cfworker.route.type.RouteType
import cn.rtast.cfworker.route.type.StringRoute
public fun WorkerApplication.route(
path: Regex,
methods: Set<HttpMethod> = HttpMethod.entries.toSet(),
block: Handler,
): Unit = run { routes.add(RegexRoute(path, block, methods)) }
public fun WorkerApplication.route(
path: String,
methods: Set<HttpMethod> = HttpMethod.entries.toSet(),
block: Handler,
): Unit = run { routes.add(StringRoute(path, block, methods)) }
public fun WorkerApplication.route(
path: String,
methods: Set<HttpMethod> = HttpMethod.entries.toSet(),
authenticator: Authenticator<HttpCredential>,
block: Handler,
): Unit = run { routes.add(StringRoute(path, block, methods, type = RouteType.String, authenticator = authenticator)) }
public fun WorkerApplication.route(
path: Regex,
methods: Set<HttpMethod> = HttpMethod.entries.toSet(),
authenticator: Authenticator<HttpCredential>,
block: Handler,
): Unit = run { routes.add(RegexRoute(path, block, methods, type = RouteType.Regex, authenticator = authenticator)) }
@@ -0,0 +1,45 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 01:04
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker.route.type
import cn.rtast.cfworker.HttpMethod
import cn.rtast.cfworker.auth.credentials.HttpCredential
import cn.rtast.cfworker.auth.provider.Authenticator
import cn.rtast.cfworker.route.Handler
import org.w3c.fetch.Request
import org.w3c.fetch.Response
internal interface AbstractRoute {
/**
* Route type
*/
val type: RouteType
/**
* Route code handle block
*/
val block: Handler
/**
* Allowed http methods
*/
val methods: Set<HttpMethod>
/**
* Endpoint authenticator, if null, no auth required
*/
val authenticator: Authenticator<HttpCredential>?
/**
* A route handler to process incoming http request
* @see Handler
*/
suspend fun handle(request: Request): Response = block.invoke(request)
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 01:04
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker.route.type
import cn.rtast.cfworker.HttpMethod
import cn.rtast.cfworker.auth.credentials.HttpCredential
import cn.rtast.cfworker.auth.provider.Authenticator
import cn.rtast.cfworker.route.Handler
internal data class RegexRoute(
val path: Regex,
override val block: Handler,
override val methods: Set<HttpMethod>,
override val type: RouteType = RouteType.Regex,
override val authenticator: Authenticator<HttpCredential>? = null,
) : AbstractRoute
@@ -0,0 +1,12 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 01:05
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker.route.type
internal enum class RouteType {
Regex, String
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2025 RTAkland
* Date: 2025/12/28 01:04
* Open Source Under Apache-2.0 License
* https://www.apache.org/licenses/LICENSE-2.0
*/
package cn.rtast.cfworker.route.type
import cn.rtast.cfworker.HttpMethod
import cn.rtast.cfworker.auth.credentials.HttpCredential
import cn.rtast.cfworker.auth.provider.Authenticator
import cn.rtast.cfworker.route.Handler
internal data class StringRoute(
val path: String,
override val block: Handler,
override val methods: Set<HttpMethod>,
override val type: RouteType = RouteType.String,
override val authenticator: Authenticator<HttpCredential>? = null,
) : AbstractRoute
@@ -0,0 +1,13 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.util
import kotlin.io.encoding.Base64
internal val String.decodeBase64String: String
get() = Base64.decode(this.encodeToByteArray()).decodeToString()
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.util
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Int8Array
/**
* Convert [ArrayBuffer] to [ByteArray]
*/
public fun ArrayBuffer.toByteArray(): ByteArray =
Int8Array(this).unsafeCast<ByteArray>()
/**
* Convert [ByteArray] to [ArrayBuffer]
*/
public fun ByteArray.toArrayBuffer(): ArrayBuffer =
Int8Array(this.toTypedArray()).unsafeCast<ArrayBuffer>()
@@ -0,0 +1,94 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
@file:OptIn(DelicateCoroutinesApi::class)
package cn.rtast.cfworker.websocket
import cn.rtast.cfworker.util.toByteArray
import cn.rtast.cfworker.websocket.response.respondSwitchingProtocol
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Uint8Array
import org.w3c.dom.CloseEvent
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
import org.w3c.fetch.Request
import org.w3c.fetch.Response
import org.w3c.fetch.ResponseInit
import org.w3c.files.Blob
public class WebsocketEventHandler(public val request: Request) {
private var onMessageBlock: (suspend (MessageEvent) -> Unit)? = null
private var onCloseBlock: (suspend (CloseEvent) -> Unit?)? = null
private var onOpenBlock: (suspend () -> Unit)? = null
private var onErrorBlock: (suspend (Event) -> Unit)? = null
private var requireUpgradeHeaderBlock: (suspend (Request) -> Response)? =
{ Response("Expected Upgrade: websocket", ResponseInit(426)) }
public val clients: MutableList<WebSocket> = mutableListOf()
public fun upgradeHeaderRequired(block: suspend (Request) -> Response) {
requireUpgradeHeaderBlock = block
}
public fun onMessage(block: suspend (MessageEvent) -> Unit) {
onMessageBlock = block
}
public fun onClose(block: suspend (CloseEvent) -> Unit?) {
onCloseBlock = block
}
public fun onOpen(block: suspend () -> Unit) {
onOpenBlock = block
}
public fun onError(block: suspend (Event) -> Unit) {
onErrorBlock = block
}
public fun close(code: Short = 1000, reason: String? = null): Unit =
this.clients.forEach { it.close(code, reason ?: "Closed") }
internal suspend fun handle(): Response {
val upgradeHeader = request.headers.get("upgrade")
if (upgradeHeader == null || upgradeHeader != "websocket")
return requireUpgradeHeaderBlock!!.invoke(request)
val pair = js("new WebSocketPair()")
val client = pair[0]
val server = pair[1]
server.accept()
clients.add(server)
GlobalScope.promise { onOpenBlock?.invoke() }
server.addEventListener("message", { e: MessageEvent -> GlobalScope.promise { onMessageBlock?.invoke(e) } })
server.addEventListener("error", { e: Event -> GlobalScope.promise { onErrorBlock?.invoke(e) } })
server.addEventListener("close", { e: CloseEvent ->
GlobalScope.promise {
onCloseBlock?.invoke(e)
clients.remove(client)
}
})
return respondSwitchingProtocol(client)
}
}
public typealias WebsocketHandler = suspend WebsocketEventHandler.() -> Unit
public fun MessageEvent.readText(): String =
this.data.unsafeCast<String>()
public fun MessageEvent.readByteArray(): ByteArray =
this.data.unsafeCast<ArrayBuffer>().toByteArray()
public fun MessageEvent.readBlob(): Blob =
this.data.unsafeCast<Blob>()
public fun MessageEvent.readUint8Array(): Uint8Array =
this.data.unsafeCast<Uint8Array>()
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.websocket
import cn.rtast.cfworker.WorkerApplication
internal data class WebsocketRoute(
val stringPath: String? = null,
val regexPath: Regex? = null,
val block: WebsocketHandler,
)
public fun WorkerApplication.webSocket(
path: Regex,
block: WebsocketHandler,
): Unit = run { websocketRoutes.add(WebsocketRoute(regexPath = path, block = block)) }
public fun WorkerApplication.webSocket(
path: String,
block: WebsocketHandler,
): Unit = run { websocketRoutes.add(WebsocketRoute(stringPath = path, block = block)) }
@@ -0,0 +1,17 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package cn.rtast.cfworker.websocket.response
import org.w3c.fetch.Response
/**
* Respond switching protocol
* Websocket
*/
internal fun respondSwitchingProtocol(client: dynamic): Response =
Response(null, js("{ status: 101, webSocket: client }"))
@@ -0,0 +1,11 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/1/3
*/
package test
class Test {
}