Kotlin lib talkide-storage-sdk přibalená do scaffoldu user appky + update Mara promptu v talkide-backend-dev.md (vrstva 2 dle CLAUDE.md) tak, aby Mara věděla, jak SDK použít a aby ho nezasahovala chybnými volánímy přes raw HTTP.
- SDK je publikováno jako Gradle dependency v scaffold
build.gradle.kts:implementation("app.talkide:talkide-storage-sdk:1.0.0"). - SDK je thin wrapper nad storage-svc HTTP API (UC-12002, UC-12003, UC-12004).
- Konfigurace přes Spring Boot auto-configuration — žádný code v
Application.ktuser appky. - Env vars
TALKIDE_APP_STORAGE_URLaTALKIDE_APP_STORAGE_TOKENinjektované K8s Secret při deployi (provisionované UC-12001).
Scope of this UC
- Definovat veřejné API
talkide-storage-sdkKotlin lib. - Definovat scaffold integraci — kde se přidá dependency, jaké env vars musí být v Deploymentu.
- Definovat Mara prompt update — co se přidá do
talkide-backend-dev.mdtak, aby Mara SDK znala. - Definovat publish workflow — jak se SDK staví a publikuje.
Public API TalkideStorageClient
package app.talkide.storage
interface TalkideStorageClient {
/** Request a presigned PUT URL. Throws [StorageException] on validation/auth/quota failures. */
fun presignUpload(key: String, contentType: String, contentLength: Long): PresignedUpload
/** Request a presigned GET URL. Throws [StorageException] on validation/auth failures. */
fun presignDownload(key: String, contentDisposition: String? = null): PresignedDownload
/** List objects under a prefix. Returns one page; call again with [PageResult.nextCursor] for next page. */
fun list(prefix: String = "", cursor: String? = null, limit: Int = 100): PageResult<StorageObject>
/** Delete an object by key. Idempotent — no error if the object did not exist. */
fun delete(key: String)
}
data class PresignedUpload(val uploadUrl: String, val key: String, val expiresAt: Instant)
data class PresignedDownload(val downloadUrl: String, val key: String, val expiresAt: Instant)
data class StorageObject(val key: String, val sizeBytes: Long, val lastModified: Instant, val etag: String)
data class PageResult<T>(val items: List<T>, val nextCursor: String?, val hasMore: Boolean)
sealed class StorageException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) {
class InvalidKey(message: String) : StorageException(message)
class QuotaExceeded(message: String) : StorageException(message)
class AuthenticationFailed(message: String) : StorageException(message)
class UpstreamError(message: String, cause: Throwable? = null) : StorageException(message, cause)
}
Spring Boot auto-configuration
src/main/kotlin/app/talkide/storage/TalkideStorageAutoConfiguration.kt:
@AutoConfiguration
@ConditionalOnClass(TalkideStorageClient::class)
@EnableConfigurationProperties(TalkideStorageProperties::class)
class TalkideStorageAutoConfiguration {
@Bean
@ConditionalOnMissingBean
fun talkideStorageClient(props: TalkideStorageProperties): TalkideStorageClient =
HttpTalkideStorageClient(props.url, props.token)
}
@ConfigurationProperties(prefix = "talkide.storage")
data class TalkideStorageProperties(
val url: String, // bind from TALKIDE_APP_STORAGE_URL via Spring relaxed binding
val token: String, // bind from TALKIDE_APP_STORAGE_TOKEN
)
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
app.talkide.storage.TalkideStorageAutoConfiguration
User appka pak jen:
@RestController
class AvatarController(private val storage: TalkideStorageClient) {
@PostMapping("/api/avatars/upload-url")
fun getUploadUrl(@RequestBody req: AvatarUploadRequest): PresignedUpload =
storage.presignUpload(
key = "uploads/avatars/${req.userId}.png",
contentType = "image/png",
contentLength = req.sizeBytes,
)
}
Scaffold integration
build.gradle.kts (user-app BE)
Scaffold generator (scaffold-backend.sh resp. statická šablona talkide-be/templates/kotlin-spring/...) přidá:
dependencies {
// ... existing ...
implementation("app.talkide:talkide-storage-sdk:1.0.0")
}
repositories {
mavenCentral()
maven { url = uri("https://repo.talkide.app/maven") } // private repo pro talkide-storage-sdk
}
application.yaml (user-app BE)
talkide:
storage:
url: ${TALKIDE_APP_STORAGE_URL}
token: ${TALKIDE_APP_STORAGE_TOKEN}
K8s Deployment env (template generated by K8sAppDeployer v platform talkide-be)
env:
- name: TALKIDE_APP_STORAGE_URL
value: "http://storage-svc-{projectSlug}.{tenantSlug}-{envSlug}.svc.cluster.local"
- name: TALKIDE_APP_STORAGE_TOKEN
valueFrom:
secretKeyRef:
name: storage-creds
key: APP_STORAGE_TOKEN
Pod labels (required for storage-svc NetworkPolicy)
User-app Deployment musí mít labely:
metadata:
labels:
talkide.app/project: "{projectSlug}"
talkide.app/role: "user-app"
Bez těchto labelů NetworkPolicy v UC-12001 zablokuje volání user-app → storage-svc.
Mara prompt update (talkide-be/plugin/agents/talkide-backend-dev.md)
CLAUDE.md vrstva 2 — AI-generovaný kód za běhu píše Theo (BE dev agent). Mara/Theo musí vědět:
Sekce k přidání do talkide-backend-dev.md:
## Storage SDK
Tvoje user appka má vždy k dispozici `TalkideStorageClient` Spring bean (z dependency `app.talkide:talkide-storage-sdk`).
**Použij ho VŽDYCKY** pro práci se soubory v user appce. NIKDY:
- Nepiš vlastní AWS S3 / Cloudflare R2 SDK kód.
- Nevolal `RestTemplate` / `WebClient` přímo proti `TALKIDE_APP_STORAGE_URL`.
- Neukládaj soubory na disk podu (pod má pouze ephemeral storage, restart = ztráta dat).
- Neukládej `TALKIDE_APP_STORAGE_TOKEN` do DB ani conversation history.
**Standardní pattern (upload):**
\`\`\`kotlin
@RestController
class FileController(private val storage: TalkideStorageClient) {
@PostMapping("/api/files/upload-url")
fun uploadUrl(@RequestBody req: UploadUrlRequest): PresignedUpload {
// klient pak udělá PUT přímo na uploadUrl s contentType a binary body
return storage.presignUpload(req.key, req.contentType, req.contentLength)
}
}
\`\`\`
**Standardní pattern (download):**
\`\`\`kotlin
@GetMapping("/api/files/{key}/download-url")
fun downloadUrl(@PathVariable key: String): PresignedDownload =
storage.presignDownload(key)
\`\`\`
**Standardní pattern (list):**
\`\`\`kotlin
@GetMapping("/api/files")
fun list(@RequestParam(required = false) prefix: String?,
@RequestParam(required = false) cursor: String?): PageResult<StorageObject> =
storage.list(prefix ?: "", cursor)
\`\`\`
**Key naming convention** (doporučení pro user appku):
- `uploads/{kategorie}/{uuid}.{ext}` — user-uploaded soubory
- `generated/{kategorie}/{date}/{name}` — AI generated nebo computed content
- NIKDY `..`, leading `/`, ani double `//` (SDK to odmítne s `StorageException.InvalidKey`).
**Error handling:**
\`\`\`kotlin
try {
storage.presignUpload(key, ct, len)
} catch (e: StorageException.QuotaExceeded) {
throw ResponseStatusException(HttpStatus.PAYMENT_REQUIRED, "Storage quota exceeded")
} catch (e: StorageException.InvalidKey) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message ?: "Invalid key")
}
\`\`\`
Pozn. ke CLAUDE.md vrstva 2: Bug v Maře / Theovi co jí v tomhle zabrání ⇒ oprava v promptu talkide-be/plugin/agents/talkide-backend-dev.md, ne ve scaffold šabloně. Prompt-as-code je bundlovaný v BE runtime → projeví se až po BE redeployi.
SDK source structure
Lokace: SDK lib žije jako Gradle modul :sdk v samostatném git repo
talkide/talkide-storage-svc (Gradle multi-module setup vedle modulu :service pro
Spring Boot mikroservis). Důvod společného repa: verzová konzistence DTO contractu mezi
service a SDK (viz ADR-027 sekce
“talkide-storage-sdk”). Alternativa — SDK v talkide-be/templates/... — byla odmítnuta,
viz ADR-027 Alternatives.
Repo layout:
talkide-storage-svc/ (git repo talkide/talkide-storage-svc)
├── settings.gradle.kts (include(":service", ":sdk"))
├── build.gradle.kts (root: shared Kotlin + Spring Boot BOM)
├── .gitlab-ci.yml (manual cost gate; service image + SDK publish jobs)
├── service/ (Spring Boot mikroservis → talkide-storage-svc:{tag} image)
│ ├── build.gradle.kts (bootJar, Docker image build)
│ ├── Dockerfile
│ └── src/main/kotlin/...
└── sdk/ (Kotlin lib → Maven artifact app.talkide:talkide-storage-sdk)
├── build.gradle.kts (publishing: GitLab Package Registry)
├── src/main/kotlin/app/talkide/storage/
│ ├── TalkideStorageClient.kt (interface)
│ ├── HttpTalkideStorageClient.kt (impl: Spring RestClient)
│ ├── TalkideStorageAutoConfiguration.kt
│ ├── TalkideStorageProperties.kt
│ ├── dto/PresignUploadRequest.kt
│ ├── dto/PresignUploadResponse.kt
│ ├── dto/PresignDownloadRequest.kt
│ ├── dto/PresignDownloadResponse.kt
│ ├── dto/ListFilesResponse.kt
│ └── exception/StorageException.kt
├── src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── src/test/kotlin/...
DTO třídy (PresignUploadRequest, atd.) sdílí mezi :service a :sdk přes Gradle module
dependency — :service má implementation(project(":sdk")) na DTO třídách. Tím je structurálně
vyloučen drift mezi tím, co služba přijímá, a tím, co SDK posílá.
Tech stack:
- Kotlin 2.x, JDK 21
- Spring Boot 3.x BOM
- Jackson (přes Spring Boot)
- HTTP klient: Spring
RestClient(synchronous, jednoduchý — bez Reactor závislosti) - Žádné AWS SDK (storage-svc je překlad layer)
Publishing:
- MVP: privátní Maven repo přes GitLab Package Registry projektu
talkide/talkide-storage-svc(https://gitlab.com/api/v4/projects/<id>/packages/maven— group-level token v scaffoldgradle.properties). DO Spaces hosting odmítnut: GitLab Package Registry je nativní řešení bez dalšího infra komponentu k údržbě. - Verze: semver, start
1.0.0. Breaking changes = major bump (předpoklad: stabilní API). Service a SDK uvolňovány společně se stejným tagem (atomický release contractu). - CI:
.gitlab-ci.ymlvtalkide/talkide-storage-svcmá samostatnéservice:build,service:image:push,sdk:publishjoby — všechnywhen: manual(CI cost gate, viz CLAUDE.md). - Verzový tag konvence:
vX.Y.Zgit tag triggeruje oba publish joby (service image + SDK Maven).
Telemetry / observability
SDK loguje (přes SLF4J, INFO level):
presignUpload(key=..., contentLength=...)— bez tokenů, bez URL.presignDownload(key=...).list(prefix=..., limit=...).delete(key=...).
Žádné metriky v MVP (Micrometer integration je follow-up).
Backward compatibility & versioning
- SDK 1.x: stable, žádné breaking changes v signature
TalkideStorageClient. - Pokud storage-svc API změní (nový field v response, deprekace), SDK přidá field jako nullable + minor version bump.
- Maře/Theovi se nikdy nesmí stát, že updated SDK rozbije starší user-app DB schema nebo controller kód.
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
user-app pod má TALKIDE_APP_STORAGE_URL + TALKIDE_APP_STORAGE_TOKEN env vars | Spring context startup | TalkideStorageClient bean je auto-konfigurován; injectovatelný do controlleru |
user-app pod nemá TALKIDE_APP_STORAGE_URL env var | Spring context startup | Spring startup fail s clear error: “talkide.storage.url is required” |
user-app volá storage.presignUpload("uploads/x.png", "image/png", 1024) | SDK volání | HTTP POST na storage-svc /api/v1/storage/presign-upload; vrácen PresignedUpload |
| storage-svc vrátí 400 STORAGE_INVALID_KEY | SDK volání | SDK throw StorageException.InvalidKey |
| storage-svc vrátí 401 AUTHENTICATION_FAILED | SDK volání | SDK throw StorageException.AuthenticationFailed (indicates env var misconfiguration) |
| storage-svc vrátí 429 STORAGE_QUOTA_EXCEEDED | SDK volání | SDK throw StorageException.QuotaExceeded |
| storage-svc vrátí 502 / connection refused | SDK volání | SDK throw StorageException.UpstreamError s cause |
user-app volá storage.list("uploads/", null, 50) | SDK volání | vrácen PageResult<StorageObject> |
user-app volá storage.delete("uploads/x.png") | SDK volání | žádný throw, void return |
user-app volá storage.presignUpload("../escape", ...) | SDK volání | SDK throw StorageException.InvalidKey (sanity check lokálně před voláním storage-svc) |
| Mara dostane prompt s “user wants to upload avatar” | Mara generuje kód | použije TalkideStorageClient.presignUpload(...) — NIKDY raw S3 SDK kód |
| Mara dostane prompt s “make file public on a URL” | Mara generuje kód | Mara vysvětlí, že MVP signed-only, nabídne TTL=60min download URL |
Out of scope (this UC)
- Async/reactive flavor SDK (
TalkideStorageClientAsyncs Mono/Coroutines) — follow-up pokud user-app potřebuje. - TypeScript verze SDK pro FE direct calls — záměrně NE (HMAC token nesmí do browseru).
- Local test mock (
MockTalkideStorageClient) — follow-up po prvních real user-app projektech. - Native image / GraalVM compatibility — Spring Boot 3 to umí, ale netestováno v MVP.
Thanks for the feedback.