diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7af71e4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,182 @@ +# CLAUDE.md — Elsa custom server + +Notes for future sessions working on this repo. Everything here is the result of +"we tried it, this is what actually worked". Read before touching versions, +authentication, or the Docker/Coolify wiring. + +## What this repo is + +Custom Elsa Workflows server + Studio, built as one .NET 9 container: + +- `src/Elsa.CustomServer/` — ASP.NET Core host. Runs the Elsa server API and + serves the Studio WASM shell from the same origin. Custom activities live in + `Activities/` and are auto-registered by `AddActivitiesFrom()`. +- `src/Elsa.Studio.Wasm/` — Blazor WebAssembly Studio shell. Referenced as a + project so `dotnet publish` copies its `wwwroot/` into the server's output. +- `Dockerfile` — multi-stage .NET 9 build. Restores BOTH csprojs before copying + sources so the WASM restore lands in cache too. +- Deployed to Coolify as an `elsa` app at `https://elsa.tes.gd`, + UUID `u3qd22pn77pxyx4yf1p5ws2i`, build_pack=dockerfile, sourced from + `gitea.tes.gd/admin/ElsaWorkflow`. + +## Adding a new activity (the whole flow) + +1. Drop a `.cs` under `src/Elsa.CustomServer/Activities/`, inheriting from + `CodeActivity`, `CodeActivity`, or `Activity` (the last for triggerable ones). + Decorate with `[Activity("Category", "Name", "Description", Kind = ActivityKind.Task)]`. +2. `git push`. +3. Fire a Coolify deploy — fire-and-forget, per global rules: + `curl -sS -X POST "$COOLIFY_URL/api/v1/deploy?uuid=$APP" -H "Authorization: Bearer $COOLIFY_KEY"` +4. Hard-refresh the browser (or open incognito — Blazor caches assets aggressively). +5. New activity appears in the Studio toolbox under its declared category. + +No `AddActivity()` needed — the assembly scan picks it up. + +## Elsa 3.x package version pinning — the tricky part + +Server packages and Studio packages have **diverged release trains**: + +- **Server**: `Elsa`, `Elsa.Http`, `Elsa.Identity`, `Elsa.EntityFrameworkCore*`, + `Elsa.CSharp`, `Elsa.JavaScript`, `Elsa.Liquid`, `Elsa.Scheduling`, + `Elsa.Workflows.Api` — top out at **3.5.3** on nuget.org. +- **Studio**: `Elsa.Studio*` — at **3.7.0**. + +Do NOT try to force everything to a single version — you'll get "package not +found". The 3.7 Studio talks to a 3.5 server just fine (protocol is +backward-compatible within v3). + +Transitive Microsoft.AspNetCore.Components pins: Studio 3.7 pulls +`Elsa.Api.Client 3.7.0-rc1` → `Microsoft.Extensions.Http.Polly 9.0.13`, which +requires `Microsoft.AspNetCore.Components >= 9.0.13`. Pin those explicitly in +the WASM csproj or NuGet errors out on downgrade. + +The `NU1603` warning about `Elsa.Api.Client 3.7.0-preview.4523 not found -> +3.7.0-rc1 resolved instead` is expected and benign — do not chase it. + +## Authentication — pick ONE login stack, not both + +Elsa Studio 3.7 ships two login modules that BOTH register a `/login` route. +Adding both raises `System.InvalidOperationException: The following routes are +ambiguous: 'login' in ...` on first WASM render. + +- **Old (do not use)**: `Elsa.Studio.Login.BlazorWasm` + `AddLoginModule()` + + `Elsa.Studio.Login.HttpMessageHandlers.AuthenticatingApiHttpMessageHandler`. +- **New (use this)**: `Elsa.Studio.Authentication.ElsaIdentity.BlazorWasm` + + `AddElsaIdentity()` **and** `Elsa.Studio.Authentication.ElsaIdentity.UI` + + `AddElsaIdentityUI()` + `Elsa.Studio.Authentication.ElsaIdentity.HttpMessageHandlers.ElsaIdentityAuthenticatingApiHttpMessageHandler`. + +You need BOTH `.BlazorWasm` (platform services / token accessor) AND `.UI` +(login page + unauthorized-redirect handler). Missing `.UI` = shell shows +"Unauthorized" with nowhere to go — no login screen. + +## Admin credentials — the empty-hash trap + +`Elsa.Identity` uses config-based users with `HashedPassword` + `HashedPasswordSalt`. +Leaving them as empty strings never validates — every login returns +`{"isAuthenticated":false}` even with the "right" password. + +Reference well-known hashes for `admin` / `password` (from +`Elsa.ServerAndStudio.Web/appsettings.json` on the v3 branch of elsa-core): + +```json +"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=", +"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=" +``` + +Both are already in `appsettings.json`. To create a real user, generate a new +hash + salt through Elsa's password hasher and overwrite via env var +(`Identity__Users__0__HashedPassword`, `Identity__Users__0__HashedPasswordSalt`). + +## Behind Coolify's Traefik — forwarded headers are mandatory + +Traefik terminates TLS and forwards the request as plain HTTP. Without +configuring forwarded headers, Kestrel thinks it's serving HTTP and every +generated URL (Blazor's `apiUrl`, redirects, links) comes out as `http://`, +which the browser refuses to fetch from an HTTPS page ("Failed to fetch", +mixed-content errors). + +Fix (already in `Program.cs`): + +```csharp +builder.Services.Configure(o => +{ + o.ForwardedHeaders = ForwardedHeaders.All; + o.KnownNetworks.Clear(); // Traefik isn't in a known private range + o.KnownProxies.Clear(); +}); +// ... +app.UseForwardedHeaders(); // must be BEFORE UseRouting / UseAuthentication +``` + +Setting `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` as an env var does the same +thing but we prefer the code path so behaviour is source-controlled. + +## SPA fallback — carve out `/elsa/api` so 404s stay 404 + +`MapFallbackToPage("/_Host")` catches EVERYTHING no other endpoint claims, +including unknown `/elsa/api/*` paths — which then return the Studio HTML with +a 200 status, confusing anything that expects JSON. + +Fix (already in `Program.cs`): + +```csharp +app.MapFallback("/elsa/api/{**rest}", () => Results.NotFound(new { error = "not_found" })); +app.MapFallbackToPage("/_Host"); +``` + +Order matters — the API-specific fallback must be registered FIRST. + +## Studio → backend wiring + +The WASM Studio reads its backend URL at runtime from a JS-injected client +config, NOT from a compile-time constant. `Pages/_Host.cshtml` renders: + +```js +window.getClientConfig = function () { return { "apiUrl": "https://elsa.tes.gd/elsa/api" }; }; +``` + +…assembled from `Hosting:BaseUrl` (or `Http:BaseUrl`) + `Hosting:ApiPrefix` +(default `elsa/api`). `Elsa.Studio.Wasm/Program.cs` reads this via `IJSRuntime` +after DI is set up and stuffs it into `BackendOptions.Url`. This lets the SAME +compiled WASM point at any origin without a rebuild — set `HTTP__BASEURL` in +Coolify env vars to change the target. + +## SQLite persistence — pin the path + +`UseSqlite()` with no connection string writes to `elsa.sqlite.db` in the +process working dir → `/app/elsa.sqlite.db` inside the container, which is +OUTSIDE the mounted `/app/App_Data` volume. Every redeploy loses all state. + +Fix (already in `Program.cs`): read `ConnectionStrings:Sqlite` (defaults to +`Data Source=App_Data/elsa.sqlite.db;Cache=Shared;`) and pass it to +`ef.UseSqlite(sqliteConnectionString)`. + +For anything real, move to Postgres — add `Elsa.EntityFrameworkCore.PostgreSql` +NuGet, switch `.UseSqlite(...)` → `.UsePostgreSql(...)`, provision a Coolify +Postgres service and set `ConnectionStrings__Sqlite` (still the config key) +to its DSN. + +## Coolify deployment quirks (repo-specific) + +- App is built via `POST /applications/public` with `build_pack=dockerfile` — + proper application, not a service. (`POST /applications/dockercompose` + creates a "service" instead and lacks the clean `docker_compose_domains` + handling — avoid.) +- `git_repository` is truncated to `owner/repo.git` on create. Always + PATCH it back with the full `https://user:token@host/owner/repo.git` after. + Confirm with a GET. +- `docker_compose_raw` (if you ever go that route) must be **base64-encoded** + at create time. `POST /applications/dockercompose` rejects it otherwise. +- Deploys are fire-and-forget — never poll from Claude. User's global rule. + +## Session-specific reminders + +- Custom activities added under `Activities/` in the SERVER project (not the + WASM project). The server's assembly scan is the one that matters for the + toolbox — Studio just reads descriptors over the API. +- Blazor WASM cache is stubborn. If a change doesn't show up after + Ctrl+Shift+R, open the site in incognito to confirm whether it's a caching + issue vs. a real failure to deploy. +- When Studio complains about auth, `curl -X POST /elsa/api/identity/login` + first — if the API returns a JWT, the server is fine and the bug is + client-side (module wiring, cache).