diff --git a/plugins/kortex/skills/ktor-skill-generation/SKILL.md b/plugins/kortex/skills/ktor-skill-generation/SKILL.md new file mode 100644 index 0000000..be07628 --- /dev/null +++ b/plugins/kortex/skills/ktor-skill-generation/SKILL.md @@ -0,0 +1,279 @@ +--- +name: ktor-skill-generation +description: Use when generating or maintaining compressed framework-documentation skills like the Ktor skill from an upstream documentation repository, including source snapshotting, minor-version pinning, splitting into main/server/client references, and verification. +--- + +# Ktor Skill Generation + +Use this skill when recreating, updating, or reviewing the generated `ktor` skill in Kortex. It documents the workflow used +to compress the official Ktor documentation into a practical Hermes skill. + +This is a **skill-authoring workflow**, not a Ktor usage guide. For actual Ktor development, load `ktor` and then its +`references/server.md` or `references/client.md` as appropriate. + +## Target Layout + +The generated Ktor skill lives in the Kortex plugin skills directory: + +```text +/home/kokoko/kortex/plugins/kortex/skills/ktor/ +├── SKILL.md +└── references/ + ├── server.md + └── client.md +``` + +Minimum split required by the user: + +- `SKILL.md` — concise entry point, source snapshot, version policy, first moves, common dependencies, minimal server and + client examples, mental model, pitfalls, verification checklist. +- `references/server.md` — compressed Ktor Server documentation. +- `references/client.md` — compressed Ktor Client documentation. + +Keep this split unless the user explicitly asks for a different layout. Additional `references/*.md`, `scripts/`, or +`generation/` files are okay when they add real maintainability. + +## Source Inputs Used + +The Ktor documentation repository was cloned locally: + +```bash +cd /home/kokoko +git clone https://github.com/ktorio/ktor-documentation.git +``` + +The generated `ktor` skill currently records this snapshot: + +- Repository: `https://github.com/ktorio/ktor-documentation` +- Local checkout: `/home/kokoko/ktor-documentation` +- Ref: `main` at the Ktor `3.5.x` documentation line +- SHA: `d118418a65e03d73ad7c2d33bb68e979cebcb4e5` +- Version variables from `v.list`: + - Ktor: `3.5.1` + - Kotlin: `2.3.21` + - Coroutines: `1.11.0` + +Primary source files inspected: + +- `v.list` for version variables. +- `ktor.tree` for the official information architecture and server/client split. +- `topics/whats-new-350.md` for Ktor 3.5 changes. +- Server topics such as `server-dependencies.topic`, `server-engines.md`, `server-routing.md`, `server-requests.md`, + `server-responses.md`, `server-serialization.md`, `server-auth.md`, `server-sessions.md`, + `server-testing.md`, `server-dependency-injection.md`. +- Client topics such as `client-dependencies.md`, `client-create-and-configure.md`, `client-engines.md`, + `client-requests.md`, `client-responses.md`, `client-serialization.md`, `client-auth.md`, + `client-response-validation.md`, `client-timeout.md`, `client-plugins.md`, `client-testing.md`. + +## Versioning Policy + +Version by **Ktor minor line**, matching the style of the `kotlin-toolchain` skill: + +- The default `SKILL.md` should be directly useful for the current supported minor line, not just a routing index. +- Record the upstream docs repository, ref/SHA, and version variables in `SKILL.md`. +- State the supported minor line explicitly, for example `Ktor 3.5.x`. +- Do not silently mix behavior-changing guidance from another minor line. +- When updating to a future minor, either update the default skill to the new minor with a new source snapshot or add a + separate minor-specific reference/snapshot if multiple lines need support. + +## Generation Workflow + +1. **Verify or clone Kortex and Ktor docs.** + + ```bash + cd /home/kokoko + test -d kortex/.git + test -d ktor-documentation/.git || git clone https://github.com/ktorio/ktor-documentation.git + git -C ktor-documentation rev-parse HEAD + git -C ktor-documentation status --short --branch + ``` + +2. **Read the existing Kortex skill conventions.** + + Use `kotlin-toolchain` as the versioning/layout precedent: + + ```text + /home/kokoko/kortex/plugins/kortex/skills/kotlin-toolchain/SKILL.md + ``` + + Important conventions copied from it: + + - source snapshot section + - explicit upstream SHA/ref + - default guide remains operational, not only an index + - minor-line/version sensitivity is called out + - linked files are referenced from `SKILL.md` + +3. **Extract docs structure from `ktor.tree`.** + + Use the TOC to decide what belongs in server vs client references. + + Server sections observed: + + - Getting started + - Developing applications + - creating/configuring server, engines, configuration, modules, DI, plugins + - routing, requests, responses + - serialization + - static content/templates + - authentication, sessions + - HTTP plugins + - WebSockets, SSE, sockets + - monitoring/admin + - running/debugging, testing, deployment, extending Ktor + + Client sections observed: + + - Getting started + - supported platforms, dependencies, create/configure, engines, plugins, SSL/proxy + - requests, responses, serialization + - auth, cookies, content encoding, cache, text/charsets, timeout, logging + - WebSockets, SSE + - monitoring, custom plugins, testing + +4. **Collect high-value details, not every paragraph.** + + The goal is compressed operational guidance, not a mirror of the docs. Keep: + + - required Maven artifacts + - canonical imports and minimal examples + - install/configure patterns + - version-specific changes and migration traps + - testing guidance + - production pitfalls + - engine/platform limitations + + Drop or aggressively summarize: + + - marketing/explanatory prose + - duplicated examples + - long tutorial narratives + - platform guides that only matter for deployment details unless they affect daily coding + +5. **Write the main `SKILL.md`.** + + Include: + + - frontmatter with `name: ktor` and a broad trigger description + - what the skill is and is not + - source snapshot + - minor-version policy + - first moves in a repo + - common coordinates and artifacts + - minimal server and client shapes + - mental model + - frequent pitfalls + - verification checklist + +6. **Write `references/server.md`.** + + Organize by tasks an agent performs: + + - setup and engines + - startup styles: `embeddedServer` and `EngineMain` + - application structure + - configuration and DI + - routing + - requests and responses + - serialization/content negotiation + - plugins + - auth/session details + - static content/templates + - WebSockets/SSE + - testing + - deployment checklist + +7. **Write `references/client.md`.** + + Organize by tasks an agent performs: + + - setup and engines + - client lifecycle + - KMP pattern + - requests and bodies + - responses and streaming + - serialization/content negotiation + - validation/errors + - timeout/retry/redirects + - default headers/user-agent + - auth/cookies/cache + - logging/tracing + - proxy/SSL + - WebSockets/SSE + - custom plugins + - testing with `MockEngine` + - engine selection notes + +8. **Verify the generated files.** + + Check file presence, frontmatter, links, and git status: + + ```bash + cd /home/kokoko/kortex + git status --short + python3 - <<'PY' + from pathlib import Path + base = Path('plugins/kortex/skills/ktor') + files = [base/'SKILL.md', base/'references/server.md', base/'references/client.md'] + for p in files: + txt = p.read_text() + print(f'{p}: {len(txt)} bytes, {txt.count(chr(10)) + 1} lines') + main = files[0].read_text() + assert main.startswith('---\n') and '\n---\n' in main + assert 'name: ktor' in main and 'description:' in main + assert '3.5.x' in main + assert 'references/server.md' in main + assert 'references/client.md' in main + print('OK') + PY + ``` + +## Implementation Notes from the Original Run + +The original generation used these concrete actions: + +1. Load `hermes-agent` because this is Hermes skill authoring. +2. Inspect `kotlin-toolchain` in `/home/kokoko/kortex/plugins/kortex/skills/kotlin-toolchain/SKILL.md` to copy the + source-snapshot/minor-version style. +3. Inspect the Ktor docs checkout with file search and direct reads. +4. Use `v.list` to capture `ktor_version`, `kotlin_version`, and `coroutines_version`. +5. Use `ktor.tree` to confirm there are official top-level `Ktor Server` and `Ktor Client` sections. +6. Summarize representative topic headings/artifacts from server and client docs. +7. Create files with direct file writes under the Kortex plugin path, not user-local `~/.hermes/skills`, because this is + an in-repository bundled plugin skill. +8. Verify with a small Python assertion script and `git status --short`. + +## Quality Bar + +A generated documentation skill is good when: + +- It can be loaded and immediately guide code changes without opening the full docs for common tasks. +- It includes enough coordinates/imports/examples to prevent common hallucinations. +- It is clear about what version/minor line it represents. +- It tells the agent when to inspect the actual project instead of relying on generic guidance. +- It splits long material into references so the main skill stays readable. +- It has a concrete verification checklist. + +## Common Pitfalls + +- Creating the skill with `skill_manage(action='create')` when the user wants it inside the Kortex repo. Use file writes + to `/home/kokoko/kortex/plugins/kortex/skills/...` for in-repo plugin skills. +- Forgetting source SHA/ref/version variables, which makes the compressed docs unverifiable later. +- Making `SKILL.md` only an index. The default guide should still contain practical first-response guidance. +- Overfitting to one tutorial and missing major docs areas from `ktor.tree`. +- Copying too much upstream prose instead of compressing to decision-making guidance. +- Mixing server and client imports/artifacts. +- Omitting `.main.kts`/KMP caveats for Ktor client dependencies. + +## Maintenance Checklist + +When updating the Ktor skill: + +- [ ] Pull or clone the desired Ktor docs snapshot. +- [ ] Record new docs SHA and version variables. +- [ ] Compare `ktor.tree` against the previous structure for new/removed topics. +- [ ] Read the `whats-new-.md` file for version-specific additions and migrations. +- [ ] Update `SKILL.md`, `references/server.md`, and `references/client.md` consistently. +- [ ] Preserve minor-line versioning language. +- [ ] Verify frontmatter and links. +- [ ] Run `git diff -- plugins/kortex/skills/ktor plugins/kortex/skills/ktor-skill-generation` before reporting. diff --git a/plugins/kortex/skills/ktor/SKILL.md b/plugins/kortex/skills/ktor/SKILL.md new file mode 100644 index 0000000..95f9f44 --- /dev/null +++ b/plugins/kortex/skills/ktor/SKILL.md @@ -0,0 +1,180 @@ +--- +name: ktor +description: Use when building, modifying, debugging, or reviewing Ktor 3.5.x server and client applications, including routing, requests/responses, plugins, serialization, authentication, sessions, WebSockets, SSE, engines, testing, deployment, and Kotlin multiplatform client work. +--- + +# Ktor + +Use this skill for Ktor framework work: Kotlin HTTP servers, HTTP clients, multiplatform clients, REST APIs, +WebSockets, SSE, serialization, authentication, plugins, testing, and deployment. + +Do not confuse Ktor with generic Kotlin, Spring Boot, ktorfit, Retrofit, OkHttp-only usage, or Kotlin Toolchain build +configuration. If the task is about `module.yaml` or the `./kotlin` build CLI, load `kotlin-toolchain` too. If the task is +a standalone `.main.kts` script using Ktor, load `main-kts` too. + +## Source Snapshot + +This skill is compressed from the official Ktor documentation checked out locally at: + +- Repository: `https://github.com/ktorio/ktor-documentation` +- Ref: `main` at the Ktor `3.5.x` documentation line +- SHA: `d118418a65e03d73ad7c2d33bb68e979cebcb4e5` +- Ktor version variable: `3.5.1` +- Kotlin version variable: `2.3.21` +- Coroutines version variable: `1.11.0` +- Server guide: `references/server.md` +- Client guide: `references/client.md` + +Version this skill by the Ktor **minor** line, like `kotlin-toolchain`: this default `SKILL.md` targets Ktor `3.5.x` and +should keep immediate operational guidance here instead of becoming only a version index. If maintaining future docs, +create or update a minor-line snapshot such as Ktor `3.6.x`; do not silently mix breaking or behavior-changing guidance +from a different minor line. + +Ktor APIs move between minor releases. When exact syntax matters, inspect the project dependencies/build files first and +prefer the docs for the project's installed Ktor version. + +## First Moves + +When working in a repository: + +1. Inspect build files first: `build.gradle.kts`, `settings.gradle.kts`, `gradle/libs.versions.toml`, `pom.xml`, + `module.yaml`, or `.main.kts` annotations. +2. Determine Ktor version and whether it is server, client, or both. +3. For server work, load `references/server.md`. +4. For client or multiplatform client work, load `references/client.md`. +5. Preserve the project's build system and existing style. Do not introduce Gradle if the project uses Kotlin Toolchain; + do not introduce Kotlin Toolchain if the project uses Gradle or Maven. +6. Add only the artifacts required for the plugins/features being installed. +7. After changes, run the project's real tests or at least a compile/build task. + +## Core Coordinates and Imports + +Ktor artifacts use Maven group `io.ktor`. In Gradle examples, use one version source: + +```kotlin +val ktorVersion = "3.5.1" +implementation("io.ktor:ktor-server-core:$ktorVersion") +implementation("io.ktor:ktor-client-core:$ktorVersion") +``` + +Common server dependencies: + +- `ktor-server-core` +- engine: `ktor-server-cio`, `ktor-server-netty`, `ktor-server-jetty-jakarta`, or `ktor-server-tomcat-jakarta` +- serialization: `ktor-server-content-negotiation` + one of `ktor-serialization-kotlinx-json`, `gson`, `jackson`, + `kotlinx-xml`, `kotlinx-cbor`, `kotlinx-protobuf` +- auth: `ktor-server-auth`, optional `ktor-server-auth-jwt`, `ktor-server-auth-ldap` +- sessions: `ktor-server-sessions` +- testing: `ktor-server-test-host` +- DI: `ktor-server-di` + +Common client dependencies: + +- `ktor-client-core` +- engine: `ktor-client-cio`, `ktor-client-okhttp`, `ktor-client-apache5`, `ktor-client-java`, + `ktor-client-jetty-jakarta`, `ktor-client-android`, `ktor-client-darwin`, `ktor-client-curl`, `ktor-client-winhttp`, + or `ktor-client-js` +- serialization: `ktor-client-content-negotiation` + one of the serialization artifacts above +- auth: `ktor-client-auth` +- logging: `ktor-client-logging` +- testing: `ktor-client-mock` + +For `.main.kts`, prefer JVM/platform leaf artifacts where needed, for example `io.ktor:ktor-client-cio-jvm:3.5.1`, +because script dependency resolution can miss Gradle module metadata for multiplatform root artifacts. + +## Server Minimal Shape + +Load `references/server.md` before non-trivial server edits. + +```kotlin +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun main() { + embeddedServer(CIO, port = 8080, host = "0.0.0.0") { + install(ContentNegotiation) { json() } + routing { + get("/health") { call.respondText("OK") } + get("/") { call.respond(mapOf("status" to "up")) } + } + }.start(wait = true) +} +``` + +Prefer route extension functions once routing grows: + +```kotlin +fun Application.module() { + configureSerialization() + configureRouting() +} + +fun Application.configureRouting() = routing { + route("/api") { get("/health") { call.respond(HttpStatusCode.OK) } } +} +``` + +## Client Minimal Shape + +Load `references/client.md` before non-trivial client edits. + +```kotlin +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.serialization.kotlinx.json.* + +suspend fun main() { + val client = HttpClient(CIO) { + install(ContentNegotiation) { json() } + } + try { + val body: String = client.get("https://example.com").body() + println(body) + } finally { + client.close() + } +} +``` + +Use a singleton/DI-managed `HttpClient` in applications. Do not create and dispose a new client per request. + +## Mental Model + +Ktor is plugin-oriented: + +- A server `Application` installs plugins and registers routes. The current request/response is an `ApplicationCall`. +- A client `HttpClient` installs plugins and makes suspending requests. The current response is an `HttpResponse`. +- Most features require both an artifact and `install(PluginName) { ... }`. +- Serialization is not implicit: install `ContentNegotiation` and choose a serializer. +- Authentication is route-scoped on the server via `authenticate { ... }`; on the client it is request-scoped/provider- + selected through the `Auth` plugin. +- Engines perform network I/O. Choose an engine compatible with the platform and features needed. + +## Frequent Pitfalls + +- Missing plugin artifact: `install(ContentNegotiation)` needs `ktor-server-content-negotiation` or + `ktor-client-content-negotiation` plus serializer artifacts. +- Wrong imports: server packages are under `io.ktor.server.*`; client packages under `io.ktor.client.*`. +- Receiving request bodies twice on server without `DoubleReceive` fails. +- Forgetting `@Serializable` or the Kotlin serialization compiler plugin when using `kotlinx.serialization`. +- Creating an `HttpClient` per request leaks resources and defeats pooling. +- In tests, use Ktor test hosts/mocks instead of binding real ports unless doing E2E. +- For Ktor 3.5 digest auth, prefer RFC 7616 APIs (`DigestAlgorithm`, `DigestQop`, algorithm-aware digest provider) over + stringly legacy MD5-only examples. + +## Verification Checklist + +- [ ] Project Ktor version and build system were inspected. +- [ ] Server/client/reference file was loaded as appropriate. +- [ ] Required artifacts match installed plugins/features. +- [ ] Code compiles against the project's Ktor minor version. +- [ ] Tests/build were run, or any blocker is reported explicitly. diff --git a/plugins/kortex/skills/ktor/references/client.md b/plugins/kortex/skills/ktor/references/client.md new file mode 100644 index 0000000..a50b35c --- /dev/null +++ b/plugins/kortex/skills/ktor/references/client.md @@ -0,0 +1,412 @@ +# Ktor Client 3.5.x + +Use this reference for Ktor HTTP client applications: engines, requests, responses, serialization, auth, cookies, +timeouts, retries, logging, WebSockets, SSE, testing, and Kotlin Multiplatform clients. + +## Client Setup + +Core dependency: + +```kotlin +implementation("io.ktor:ktor-client-core:$ktorVersion") +``` + +Add one or more engines appropriate to the target platform: + +- JVM/Android/Native/JS/WasmJs: `ktor-client-cio` +- JVM/Android: `ktor-client-okhttp` +- JVM: `ktor-client-apache5`, `ktor-client-java`, `ktor-client-jetty-jakarta` +- Android: `ktor-client-android` +- Apple/native: `ktor-client-darwin` +- Native: `ktor-client-curl`, `ktor-client-winhttp` +- JavaScript: `ktor-client-js` + +For Gradle multiplatform, Gradle resolves platform-specific artifacts automatically. For Maven or `.main.kts`, use +platform-specific coordinates where necessary, for example `ktor-client-cio-jvm`. + +## Create, Configure, Close + +```kotlin +val client = HttpClient(CIO) { + expectSuccess = true + engine { + requestTimeout = 15_000 + } + install(ContentNegotiation) { json() } +} + +try { + val text: String = client.get("https://example.com").body() +} finally { + client.close() +} +``` + +Guidance: + +- Reuse `HttpClient`; do not create one per request. +- Close it on application shutdown or use DI/lifecycle management. +- Install plugins once at construction time. +- Use explicit engine selection when behavior matters; default engine selection is convenient but less predictable. + +## Multiplatform Pattern + +In KMP, put common configuration in `commonMain` and engine dependencies in platform source sets: + +```kotlin +// commonMain +expect fun httpClient(config: HttpClientConfig<*>.() -> Unit = {}): HttpClient + +fun apiClient() = httpClient { + install(ContentNegotiation) { json() } + install(HttpTimeout) { requestTimeoutMillis = 15_000 } +} +``` + +```kotlin +// jvmMain/androidMain/etc. +actual fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient = HttpClient(CIO) { + config(this) +} +``` + +Preserve the project's existing KMP pattern. Some projects use dependency injection instead of `expect/actual` factories. + +## Requests + +Basic verbs: + +```kotlin +val response: HttpResponse = client.get("https://api.example.com/users") +val created: User = client.post("https://api.example.com/users") { + contentType(ContentType.Application.Json) + setBody(CreateUser("Ada")) +}.body() +``` + +URL construction: + +```kotlin +client.get { + url { + protocol = URLProtocol.HTTPS + host = "api.example.com" + path("v1", "users") + parameters.append("page", "1") + fragment = "top" + } +} +``` + +Request parameters: + +```kotlin +client.get("https://api.example.com") { + header(HttpHeaders.Accept, ContentType.Application.Json) + bearerAuth(token) + cookie("tracking", "off") +} +``` + +Bodies: + +- Text: `setBody("hello")` with an appropriate `contentType`. +- Objects: requires `ContentNegotiation` and serializer. +- Forms: `submitForm(...)` for URL-encoded forms. +- File/multipart: `submitFormWithBinaryData(...)` or `MultiPartFormDataContent(...)`. +- Binary/streaming: use byte arrays, channels, or IO APIs appropriate to the platform. + +Parallel requests are just coroutines; use structured concurrency and cancellation. + +## Responses + +```kotlin +val response = client.get("https://api.example.com/users/1") +println(response.status) +println(response.headers[HttpHeaders.ContentType]) + +val dto: User = response.body() +val text: String = response.bodyAsText() +``` + +Streaming: + +```kotlin +client.prepareGet("https://example.com/large.bin").execute { response -> + val channel: ByteReadChannel = response.body() + while (!channel.isClosedForRead) { + val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()) + // process and release packet according to ktor-io APIs + } +} +``` + +Always stream large downloads/uploads; do not load unbounded content into memory. + +## Content Negotiation and Serialization + +Dependencies: + +```kotlin +implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") +implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") +``` + +Install: + +```kotlin +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +data class User(val id: Long, val name: String) + +val client = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + explicitNulls = false + }) + } +} +``` + +Alternative serializers: Gson, Jackson, Kotlinx XML, CBOR, ProtoBuf. Match the artifact to the install call and payload +format. With Kotlinx serialization, ensure models have `@Serializable` and the Kotlin serialization compiler plugin is +configured in real projects. + +## Response Validation and Errors + +`expectSuccess = true` makes Ktor throw for non-2xx responses. Default exception types include redirect, client request, +and server response exceptions. + +```kotlin +val client = HttpClient(CIO) { + expectSuccess = true + HttpResponseValidator { + validateResponse { response -> + if (response.status == HttpStatusCode.Accepted) return@validateResponse + } + handleResponseExceptionWithRequest { cause, request -> + // map/log/rethrow + } + } +} +``` + +Use typed domain errors at app boundaries. Avoid swallowing response bodies needed for debugging; read them carefully +because response streams may be single-use. + +## Timeouts, Retries, Redirects + +Timeouts: + +```kotlin +install(HttpTimeout) { + requestTimeoutMillis = 15_000 + connectTimeoutMillis = 5_000 + socketTimeoutMillis = 15_000 +} +``` + +Retries: + +```kotlin +install(HttpRequestRetry) { + retryOnServerErrors(maxRetries = 3) + exponentialDelay() +} +``` + +Redirects are handled by `HttpRedirect`; configure if the app needs strict redirect behavior or auth/header preservation +rules. Be careful retrying non-idempotent POST/PUT/PATCH requests. + +## Default Request, Headers, User Agent + +```kotlin +install(DefaultRequest) { + url("https://api.example.com/v1/") + header(HttpHeaders.Accept, ContentType.Application.Json) +} +install(UserAgent) { agent = "my-app/1.0" } +``` + +Use `DefaultRequest` for base URL and headers shared by all calls. Keep per-request auth/correlation values explicit +unless the project has an interceptor/plugin for them. + +## Authentication + +Dependency: + +```kotlin +implementation("io.ktor:ktor-client-auth:$ktorVersion") +``` + +Basic: + +```kotlin +install(Auth) { + basic { + credentials { BasicAuthCredentials(username = "user", password = "pass") } + sendWithoutRequest { request -> request.url.host == "api.example.com" } + } +} +``` + +Bearer: + +```kotlin +install(Auth) { + bearer { + loadTokens { BearerTokens(accessToken, refreshToken) } + refreshTokens { + // call token endpoint, return BearerTokens or null + } + sendWithoutRequest { request -> request.url.host == "api.example.com" } + } +} +``` + +Digest is supported through the client auth plugin. Ktor 3.5 server Digest follows RFC 7616; if interoperating with it, +prefer modern algorithms and test against the real challenge headers. + +Token caching exists inside providers. Clear cached tokens or control caching behavior when implementing logout, account +switching, or token revocation. + +## Cookies and Caching + +Cookies: + +```kotlin +install(HttpCookies) // in-memory by default +``` + +Use a persistent cookie storage only when the application requires it and the platform supports secure storage. + +Caching: + +```kotlin +install(HttpCache) +``` + +Treat client-side caching as behavior-changing. Verify headers and invalidation semantics. + +## Logging, Call IDs, OpenTelemetry + +Logging dependency: + +```kotlin +implementation("io.ktor:ktor-client-logging:$ktorVersion") +``` + +Install: + +```kotlin +install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.INFO + sanitizeHeader { header -> header == HttpHeaders.Authorization } +} +``` + +Never log tokens, cookies, API keys, or PII. Use `sanitizeHeader` and project logging policy. + +Use Call ID/tracing plugins or OpenTelemetry where the service already has distributed tracing. Propagate correlation IDs +explicitly when integrating with server `CallId`. + +## Content Encoding, BOM, Text + +- `ContentEncoding` handles gzip/deflate/br where configured/supported. +- BOM remover handles byte-order marks in text responses. +- Plain text/charsets plugin support exists for controlling text conversion. + +Only add these plugins when there is a concrete encoding/interop issue. + +## Proxy and SSL + +Proxy support depends on engine. Configure at engine/client level according to the chosen engine. SSL customization also +varies by engine/platform. Prefer standard trust stores; custom trust managers/pinning should be security-reviewed. + +## WebSockets + +Dependency depends on client core/engine support; add WebSocket artifacts if the project requires them. + +```kotlin +val client = HttpClient(CIO) { install(WebSockets) } + +client.webSocket("wss://example.com/ws") { + send("hello") + for (frame in incoming) { + if (frame is Frame.Text) println(frame.readText()) + } +} +``` + +For typed WebSocket messages, install/configure WebSocket serialization and test both ends. + +## Server-Sent Events + +Use SSE for one-way event streams: + +- good for notifications/progress streams +- simpler than WebSockets for server-to-client only +- needs reconnect/heartbeat/backoff strategy +- beware proxy buffering and mobile/background lifecycle behavior + +## Custom Client Plugins + +Use a custom plugin for cross-cutting behavior such as signing, correlation IDs, custom metrics, request/response mapping, +or tenant headers. Do not implement custom plugins for one-off per-request logic. + +General shape: + +```kotlin +val MyPlugin = createClientPlugin("MyPlugin") { + onRequest { request, _ -> + request.headers.append("X-My-Header", "value") + } +} +``` + +## Testing + +Dependency: + +```kotlin +testImplementation("io.ktor:ktor-client-mock:$ktorVersion") +``` + +Use `MockEngine`: + +```kotlin +val mockEngine = MockEngine { request -> + respond( + content = """{"id":1,"name":"Ada"}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) +} + +val client = HttpClient(mockEngine) { + install(ContentNegotiation) { json() } +} + +val user: User = client.get("https://api.example.com/users/1").body() +``` + +Share production client configuration by extracting a `HttpClientConfig<*>.() -> Unit` block or factory. Test retry, +timeout, serialization, auth, and error mapping without real network calls whenever possible. + +## Engine Selection Notes + +- CIO is broad and often the default choice for simple clients. +- OkHttp is common on JVM/Android and supports features such as custom DNS resolvers in Ktor 3.5 docs. +- Apache5 is JVM-oriented and also has custom DNS resolver support in 3.5 docs. +- Java engine uses JDK HTTP client. +- Darwin is natural on Apple platforms. +- JS engine follows browser/JS platform limitations. + +Feature limitations vary by engine: HTTP/2, WebSockets, security customization, proxy support, logging, and timeout +semantics are not identical. If a task depends on one of those, verify against the chosen engine docs and run a real test. diff --git a/plugins/kortex/skills/ktor/references/server.md b/plugins/kortex/skills/ktor/references/server.md new file mode 100644 index 0000000..ff7f8ec --- /dev/null +++ b/plugins/kortex/skills/ktor/references/server.md @@ -0,0 +1,434 @@ +# Ktor Server 3.5.x + +Use this reference for Ktor server applications: routing, request/response handling, plugins, serialization, +authentication, sessions, WebSockets, SSE, testing, and deployment. + +## Server Setup + +Core dependency: + +```kotlin +implementation("io.ktor:ktor-server-core:$ktorVersion") +``` + +Choose an engine and add exactly one unless the project intentionally supports several: + +- CIO: `io.ktor:ktor-server-cio` +- Netty: `io.ktor:ktor-server-netty` +- Jetty Jakarta: `io.ktor:ktor-server-jetty-jakarta` +- Tomcat Jakarta: `io.ktor:ktor-server-tomcat-jakarta` + +Two common startup styles: + +```kotlin +// Embedded, config in code. +fun main() = embeddedServer(CIO, port = 8080, host = "0.0.0.0") { + module() +}.start(wait = true) + +fun Application.module() { + configureRouting() +} +``` + +```kotlin +// EngineMain, config in application.conf/application.yaml. +fun main(args: Array) = EngineMain.main(args) + +fun Application.module() { + configureRouting() +} +``` + +Use `EngineMain` when deployment/runtime configuration should come from files or env. Use `embeddedServer` for direct +code-driven startup, CLIs, or simple services. + +## Application Structure + +Recommended pattern: + +```kotlin +fun Application.module() { + configureSerialization() + configureSecurity() + configureHTTP() + configureRouting() +} + +fun Application.configureRouting() = routing { + route("/api") { + healthRoutes() + userRoutes() + } +} + +fun Route.healthRoutes() { + get("/health") { call.respondText("OK") } +} +``` + +Keep route handlers small. Move domain logic to services. Inject dependencies through constructor/DI instead of global +singletons where possible. + +## Configuration + +Ktor supports config in code and config files. In 3.5, `ApplicationConfig.getAs()` can deserialize root configuration +into a data class when the project has the right config serialization setup. + +Common values: + +- host/port +- deployment mode/development mode +- module names +- database/API secrets via environment variables +- plugin-specific settings + +Do not hardcode secrets in `application.conf`, YAML, or source. Prefer env-backed config. + +## Dependency Injection + +Ktor 3.5 docs include built-in DI via `ktor-server-di`. + +Use DI for services, repositories, clients, and lifecycle-managed resources. Test DI wiring with Ktor test application +rather than booting the production server. + +Typical lifecycle guidance: + +- Register long-lived resources at application startup. +- Resolve dependencies inside route modules or handlers. +- Close/dispose resources on application stop when not handled by the DI container. + +If a project already uses Koin, Dagger, Guice, Spring, or manual constructor injection, preserve that style unless the +user asks to migrate. + +## Routing + +Install/define routing: + +```kotlin +fun Application.configureRouting() = routing { + get("/") { call.respondText("Hello") } + route("/users") { + get { /* list */ } + get("/{id}") { + val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest) + } + post { /* create */ } + } +} +``` + +Path patterns: + +- `{id}` captures one segment: `/users/{id}` +- `{...}` tailcard captures remaining path segments +- `*` wildcard matches one segment without capturing +- Regex routes are available for advanced matching + +Use typed `Resources` for type-safe routing when routes are shared or URL construction must be safe. Add the resource +plugin/artifact required by the project before using it. + +## Requests + +Common request APIs: + +```kotlin +val id = call.parameters["id"] +val q = call.request.queryParameters["q"] +val header = call.request.headers[HttpHeaders.Authorization] +val cookie = call.request.cookies["session"] +val dto = call.receive() +``` + +Body handling: + +- Raw text/bytes/channels: use `receiveText`, byte/channel APIs, or multipart APIs. +- Objects: requires `ContentNegotiation` plus serializer. +- Forms: use `receiveParameters()` for `application/x-www-form-urlencoded`. +- Multipart: iterate parts and dispose file parts/resources after processing. + +A request body is normally consumable once. If you truly need multiple reads, install `DoubleReceive`, but prefer to read +once and pass a parsed value onward. + +## Responses + +Common response APIs: + +```kotlin +call.respond(HttpStatusCode.Created, dto) +call.respondText("OK", ContentType.Text.Plain) +call.respondBytes(bytes, ContentType.Application.OctetStream) +call.respondFile(file) +call.respondRedirect("/login") +call.response.headers.append(HttpHeaders.CacheControl, "no-store") +call.response.cookies.append("name", "value") +``` + +Objects require `ContentNegotiation`. For HTML, either use `respondHtml`/HTML DSL, templates, or static content depending +on the project style. + +## Content Negotiation and Serialization + +Dependencies: + +```kotlin +implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") +implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") +``` + +Install: + +```kotlin +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +data class UserDto(val id: Long, val name: String) + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + explicitNulls = false + prettyPrint = false + }) + } +} +``` + +Alternative serializers: Gson, Jackson, Kotlinx XML, CBOR, ProtoBuf. Match the artifact and install call. + +## Plugins + +Plugin rule: add dependency if separate, import correct server package, then `install(Plugin) { ... }`. + +Common HTTP plugins: + +```kotlin +install(DefaultHeaders) +install(Compression) { gzip() } +install(CORS) { + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowHeader(HttpHeaders.ContentType) + anyHost() // development only; restrict in production +} +install(StatusPages) { + exception { call, cause -> + call.respond(HttpStatusCode.BadRequest, mapOf("error" to cause.message)) + } + status(HttpStatusCode.NotFound) { call, _ -> call.respondText("Not found") } +} +``` + +Other server plugins/topics: + +- `PartialContent` for range requests +- `CachingHeaders` and `ConditionalHeaders` +- `HSTS` and `HttpsRedirect` +- `ForwardedHeaders`/`XForwardedHeaders` behind proxies +- `RateLimit` for request limiting +- `RequestValidation` for input validation +- `MethodOverride` for `X-HTTP-Method-Override` +- `CallLogging`, `CallId`, Micrometer/Dropwizard metrics, OpenTelemetry + +Be conservative with CORS and forwarded headers in production. Only trust proxy headers if the app is actually behind a +trusted proxy/load balancer. + +## Authentication and Authorization + +Dependency: + +```kotlin +implementation("io.ktor:ktor-server-auth:$ktorVersion") +``` + +Optional: + +```kotlin +implementation("io.ktor:ktor-server-auth-jwt:$ktorVersion") +implementation("io.ktor:ktor-server-auth-ldap:$ktorVersion") +``` + +Install and protect routes: + +```kotlin +install(Authentication) { + basic("basic") { + realm = "admin" + validate { credentials -> + if (credentials.name == "admin" && credentials.password == "secret") UserIdPrincipal(credentials.name) + else null + } + } +} + +routing { + authenticate("basic") { + get("/admin") { + val principal = call.principal() + call.respondText("Hello ${principal?.name}") + } + } +} +``` + +Supported providers include Basic, Digest, Bearer, Form, Session, JWT, LDAP, OAuth, and custom providers. + +Ktor 3.5 Digest auth is RFC 7616-oriented: + +```kotlin +digest("auth") { + realm = "MyRealm" + algorithms = listOf(DigestAlgorithm.SHA_512_256, DigestAlgorithm.MD5) + digestProvider { userName, realm, algorithm -> + val password = lookupPassword(userName) ?: return@digestProvider null + algorithm.toDigester().digest("$userName:$realm:$password".toByteArray()) + } +} +``` + +For new code, prefer SHA-256/SHA-512-256 over MD5 unless legacy compatibility requires MD5. + +Custom providers in 3.5 can use a suspending `authenticate { context -> ... }` lambda. + +## Sessions + +Dependency: + +```kotlin +implementation("io.ktor:ktor-server-sessions:$ktorVersion") +``` + +Example: + +```kotlin +data class UserSession(val userId: Long) + +install(Sessions) { + cookie("user_session") { + cookie.httpOnly = true + cookie.secure = true + cookie.extensions["SameSite"] = "lax" + // In 3.5, sessions can be configured to send cookies only when modified. + } +} + +routing { + get("/me") { + val session = call.sessions.get() ?: return@get call.respond(HttpStatusCode.Unauthorized) + call.respond(session) + } + post("/logout") { + call.sessions.clear() + call.respond(HttpStatusCode.NoContent) + } +} +``` + +Session payload can be cookie/header-based and client-side or server-side. Protect client-side payloads with signing or +signing+encryption. Use server-side storage for sensitive or large session state. + +## Static Content, SPA, Templates + +Static content: + +```kotlin +routing { + staticResources("/static", "static") + singlePageApplication { + react("build/dist") + } +} +``` + +Templating options include HTML DSL, CSS DSL, FreeMarker, Velocity, Mustache, Thymeleaf, Pebble, and JTE. Add the +matching artifact and keep template rendering separate from API DTO serialization. + +## WebSockets + +Dependency: + +```kotlin +implementation("io.ktor:ktor-server-websockets:$ktorVersion") +``` + +Example: + +```kotlin +install(WebSockets) + +routing { + webSocket("/ws") { + send("connected") + for (frame in incoming) { + if (frame is Frame.Text) send("echo: ${frame.readText()}") + } + } +} +``` + +For typed messages, add WebSocket serialization support and use the docs-specific serialization APIs. Deflate and custom +extensions are available but should be explicitly needed and tested with clients. + +## Server-Sent Events + +SSE is appropriate for one-way server-to-client event streams. Prefer SSE over WebSockets when the client only listens. +Use WebSockets for bidirectional interactive communication. + +Ensure responses are not buffered by proxies and that heartbeat/retry behavior matches deployment constraints. + +## Testing + +Dependency: + +```kotlin +testImplementation("io.ktor:ktor-server-test-host:$ktorVersion") +testImplementation(kotlin("test")) +``` + +Basic test: + +```kotlin +class ApplicationTest { + @Test + fun health() = testApplication { + application { module() } + val response = client.get("/health") + assertEquals(HttpStatusCode.OK, response.status) + } +} +``` + +Use the test host to: + +- load modules explicitly or from config +- install extra test routes/plugins +- customize environment/config +- configure the test client +- test JSON POST/PUT, forms, multipart, cookies, HTTPS, and WebSockets +- mock external services where practical + +Do not bind real ports for unit/integration tests unless testing deployment wiring or E2E behavior. + +## Deployment and Packaging + +Options: + +- fat JAR via Gradle Shadow or Maven Assembly +- Gradle application plugin/distributions +- WAR for servlet containers +- GraalVM native image for supported cases +- Docker/Docker Compose +- cloud targets such as Azure App Service, Google App Engine, Heroku, Dokku, Sevalla, Elastic Beanstalk + +Production checklist: + +- configure host/port from environment +- do not enable development-only CORS or debug logging +- configure TLS at proxy/load balancer or Ktor as appropriate +- trust forwarded headers only behind trusted proxies +- expose `/health` and metrics/tracing if needed +- set shutdown behavior and resource cleanup