From 946c0d47192bb4abcf70ec56b8ed06e05044c80a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 13:49:09 +0000 Subject: [PATCH] iili: rename the shortener from r3dr, and answer HEAD (#1359, #1433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The domain, the API, the compose service, the image, the OTEL service name and the public paths all read iili now: /iili/v1/shorten and /iili/v1/r/{slug} replace /r3dr/v2/*, with no alias, so both frontends move with the host. The Postgres role and database keep the r3dr_v2 names the live rows were provisioned under — renaming those is an operation, not a rename — and R3DR_V2_DB_PASSWORD stays a host secret. HEAD on a redirect returned 405: the router buckets by exact method and only GET was registered, so unfurlers never saw the Location. A shim answers HEAD from the GET routes and drops the body, which a keep-alive connection cannot carry on a HEAD response. Wire tests pin the 302 and the 404; without the shim the first fails with 405. The deploy guard collapses to one rule now that the only legitimate r3dr in the deploy surface is the database identifier — which also retires the token list whose r3dr_v2 exemption matched nothing. golang-lru and lib/pq leave go.mod and the gazelle use_repo list; the Go service that imported them is gone. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01ST7XLGWstRE4CfUF84ijKD --- .github/workflows/branch.yml | 10 +-- .gitignore | 1 - README.md | 2 +- bazel/go.MODULE.bazel | 2 - bazel/smithy.MODULE.bazel | 4 +- deploy/consolidated/Caddyfile | 29 +++---- deploy/consolidated/README.md | 18 ++--- deploy/consolidated/compose.yaml | 22 +++--- deploy/consolidated/deploy_config_test.go | 60 +++++--------- domains/iili/README.md | 15 ++++ .../r3dr_v2 => iili/apis/iili}/BUILD.bazel | 32 ++++---- .../apis/r3dr_v2 => iili/apis/iili}/README.md | 31 ++++---- .../r3dr_v2 => iili/apis/iili}/encoding.cc | 6 +- .../r3dr_v2 => iili/apis/iili}/encoding.h | 10 +-- .../apis/iili}/encoding_test.cc | 6 +- .../apis/r3dr_v2 => iili/apis/iili}/main.cc | 46 +++++------ .../r3dr_v2 => iili/apis/iili}/migrations.cc | 6 +- .../r3dr_v2 => iili/apis/iili}/migrations.h | 10 +-- .../iili/model/bindings/simplerestjson.smithy | 10 +++ .../apis/iili/model/iili.smithy} | 17 ++-- .../apis/iili}/pg_url_store.cc | 8 +- .../r3dr_v2 => iili/apis/iili}/pg_url_store.h | 12 +-- .../apis/iili}/pg_url_store_test.cc | 8 +- .../apis/iili}/production_chain_test.cc | 22 +++--- .../r3dr_v2 => iili/apis/iili}/shortener.cc | 8 +- .../r3dr_v2 => iili/apis/iili}/shortener.h | 12 +-- .../apis/iili}/shortener_test.cc | 8 +- .../apis/iili}/smithy_handler.cc | 19 ++++- domains/iili/apis/iili/smithy_handler.h | 49 ++++++++++++ .../apis/iili}/smithy_handler_test.cc | 12 +-- .../r3dr_v2 => iili/apis/iili}/test_support.h | 16 ++-- .../r3dr_v2 => iili/apis/iili}/url_store.h | 10 +-- .../r3dr_v2 => iili/apis/iili}/wire_test.cc | 79 +++++++++++-------- .../{r3dr => iili}/apps/iili_web/.gitignore | 0 .../{r3dr => iili}/apps/iili_web/BUILD.bazel | 0 .../{r3dr => iili}/apps/iili_web/README.md | 17 ++-- .../{r3dr => iili}/apps/iili_web/index.html | 0 .../apps/iili_web/package-lock.json | 4 +- .../{r3dr => iili}/apps/iili_web/package.json | 0 .../{r3dr => iili}/apps/iili_web/src/App.tsx | 0 .../apps/iili_web/src/__tests__/App.test.tsx | 0 .../src/__tests__/CopyButton.test.tsx | 0 .../src/__tests__/ShortenCard.test.tsx | 0 .../apps/iili_web/src/__tests__/api.test.ts | 2 +- .../iili_web/src/__tests__/expiry.test.ts | 0 .../iili_web/src/__tests__/recent.test.ts | 0 .../iili_web/src/__tests__/worker.test.ts | 0 .../{r3dr => iili}/apps/iili_web/src/api.ts | 6 +- .../iili_web/src/components/CopyButton.tsx | 0 .../iili_web/src/components/RecentLinks.tsx | 0 .../iili_web/src/components/ShortenCard.tsx | 0 .../apps/iili_web/src/expiry.ts | 0 .../apps/iili_web/src/index.css | 0 .../{r3dr => iili}/apps/iili_web/src/main.tsx | 0 .../apps/iili_web/src/recent.ts | 0 .../apps/iili_web/src/setupTests.ts | 0 .../apps/iili_web/src/vite-env.d.ts | 0 .../apps/iili_web/tsconfig.json | 0 .../apps/iili_web/vite.config.ts | 0 .../apps/iili_web/worker/index.d.ts | 0 .../apps/iili_web/worker/index.js | 2 +- .../apps/iili_web/wrangler.toml | 2 +- domains/platform/apis/prom_proxy/registry.go | 18 ++--- .../platform/apis/prom_proxy/registry_test.go | 14 ++-- .../apis/prom_proxy/service_handlers_test.go | 2 +- domains/r3dr/README.md | 15 ---- .../model/bindings/simplerestjson.smithy | 10 --- domains/r3dr/apis/r3dr_v2/smithy_handler.h | 42 ---------- go.mod | 2 - go.sum | 6 -- 70 files changed, 370 insertions(+), 372 deletions(-) create mode 100644 domains/iili/README.md rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/BUILD.bazel (90%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/README.md (66%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/encoding.cc (91%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/encoding.h (77%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/encoding_test.cc (96%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/main.cc (76%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/migrations.cc (94%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/migrations.h (61%) create mode 100644 domains/iili/apis/iili/model/bindings/simplerestjson.smithy rename domains/{r3dr/apis/r3dr_v2/model/r3dr_v2.smithy => iili/apis/iili/model/iili.smithy} (74%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/pg_url_store.cc (95%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/pg_url_store.h (83%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/pg_url_store_test.cc (98%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/production_chain_test.cc (94%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/shortener.cc (93%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/shortener.h (88%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/shortener_test.cc (97%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/smithy_handler.cc (74%) create mode 100644 domains/iili/apis/iili/smithy_handler.h rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/smithy_handler_test.cc (75%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/test_support.h (84%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/url_store.h (81%) rename domains/{r3dr/apis/r3dr_v2 => iili/apis/iili}/wire_test.cc (74%) rename domains/{r3dr => iili}/apps/iili_web/.gitignore (100%) rename domains/{r3dr => iili}/apps/iili_web/BUILD.bazel (100%) rename domains/{r3dr => iili}/apps/iili_web/README.md (79%) rename domains/{r3dr => iili}/apps/iili_web/index.html (100%) rename domains/{r3dr => iili}/apps/iili_web/package-lock.json (99%) rename domains/{r3dr => iili}/apps/iili_web/package.json (100%) rename domains/{r3dr => iili}/apps/iili_web/src/App.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/__tests__/App.test.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/__tests__/CopyButton.test.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/__tests__/ShortenCard.test.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/__tests__/api.test.ts (98%) rename domains/{r3dr => iili}/apps/iili_web/src/__tests__/expiry.test.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/src/__tests__/recent.test.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/src/__tests__/worker.test.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/src/api.ts (92%) rename domains/{r3dr => iili}/apps/iili_web/src/components/CopyButton.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/components/RecentLinks.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/components/ShortenCard.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/expiry.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/src/index.css (100%) rename domains/{r3dr => iili}/apps/iili_web/src/main.tsx (100%) rename domains/{r3dr => iili}/apps/iili_web/src/recent.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/src/setupTests.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/src/vite-env.d.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/tsconfig.json (100%) rename domains/{r3dr => iili}/apps/iili_web/vite.config.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/worker/index.d.ts (100%) rename domains/{r3dr => iili}/apps/iili_web/worker/index.js (86%) rename domains/{r3dr => iili}/apps/iili_web/wrangler.toml (96%) delete mode 100644 domains/r3dr/README.md delete mode 100644 domains/r3dr/apis/r3dr_v2/model/bindings/simplerestjson.smithy delete mode 100644 domains/r3dr/apis/r3dr_v2/smithy_handler.h diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 5a63f895e..bd2c31f8a 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -363,22 +363,22 @@ jobs: with: node-version: '24' cache: 'npm' - cache-dependency-path: domains/r3dr/apps/iili_web/package-lock.json + cache-dependency-path: domains/iili/apps/iili_web/package-lock.json - name: Install dependencies - working-directory: domains/r3dr/apps/iili_web + working-directory: domains/iili/apps/iili_web run: npm ci - name: Typecheck - working-directory: domains/r3dr/apps/iili_web + working-directory: domains/iili/apps/iili_web run: npm run typecheck - name: Run tests - working-directory: domains/r3dr/apps/iili_web + working-directory: domains/iili/apps/iili_web run: npm test - name: Build - working-directory: domains/r3dr/apps/iili_web + working-directory: domains/iili/apps/iili_web run: npm run build # iOS targets can't build on Ubuntu; run Swift tests on macOS when relevant files change. diff --git a/.gitignore b/.gitignore index 925d2172b..839157120 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ out vcpkg_installed conan rust-project.json -go/r3dr/deploy/deploy.sh models checkpoints diff --git a/README.md b/README.md index 8f6b6f54b..3a97eabb4 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The repository is organized into **domains**, each containing its own libraries, - [**🎨 Graphics**](domains/graphics/README.md): Ray tracers, image processing, and rendering tools. - [**🎮 Games**](domains/games/README.md): Game engines, chess tools, and multiplayer backends. - [**🌐 Platform**](domains/platform/README.md): Core infrastructure, gRPC/HTTP wrappers, and utilities. -- [**🔗 r3dr**](domains/r3dr/README.md): URL shortening ecosystem. +- [**🔗 iili**](domains/iili/README.md): URL shortening ecosystem. - [**💬 Chat**](domains/chat/README.md): Real-time communication services. - [**🤖 AI**](domains/ai/README.md): Neural network and machine learning experiments. - [**🏆 Leet**](domains/leet/README.md): Algorithmic solutions and C utility libraries. diff --git a/bazel/go.MODULE.bazel b/bazel/go.MODULE.bazel index 5cfb1192a..f344f6d59 100644 --- a/bazel/go.MODULE.bazel +++ b/bazel/go.MODULE.bazel @@ -17,8 +17,6 @@ use_repo( "com_github_golang_jwt_jwt_v5", "com_github_google_uuid", "com_github_gorilla_websocket", - "com_github_hashicorp_golang_lru_v2", - "com_github_lib_pq", "com_github_prometheus_client_golang", "com_github_prometheus_common", "com_github_stretchr_testify", diff --git a/bazel/smithy.MODULE.bazel b/bazel/smithy.MODULE.bazel index bbaa9b749..8c0295ddb 100644 --- a/bazel/smithy.MODULE.bazel +++ b/bazel/smithy.MODULE.bazel @@ -7,8 +7,8 @@ bazel_dep(name = "smithy_cpp", version = "0.0.0") # Evaluation: https://github.com/muchq/MoonBase/issues/1168 # # Pinned past muchq/smithy-cpp#187 (46bcd4d): modeled 3xx redirects — a -# typed success carrying Location instead of a GenericError. r3dr_v2's -# GET /r3dr/v2/r/{slug} needs it (#1359). +# typed success carrying Location instead of a GenericError. iili's +# GET /iili/v1/r/{slug} needs it (#1359). # # Carries muchq/smithy-cpp#173 (4208f791): terminal transitions fire the # parked send before the parked receive so End()/Close cannot deadlock when a diff --git a/deploy/consolidated/Caddyfile b/deploy/consolidated/Caddyfile index 2ad0c2f5a..e0d621ac6 100644 --- a/deploy/consolidated/Caddyfile +++ b/deploy/consolidated/Caddyfile @@ -59,14 +59,14 @@ api.muchq.com { path /v2/analyze } - @post_r3dr_shorten { + @post_iili_shorten { method POST - path /r3dr/v2/shorten + path /iili/v1/shorten } - @get_r3dr_redirect { + @get_iili_redirect { method GET - path /r3dr/v2/r/* + path /iili/v1/r/* } @ws_thoughts { @@ -90,7 +90,7 @@ api.muchq.com { method OPTIONS } - # Browser origins that call this API: muchq.com pages (incl. /r3dr) + # Browser origins that call this API: muchq.com pages (incl. /iili) # and the iili.uk SPA (plus its vite dev — every API here is public # and credential-less, so a localhost origin grants nothing curl # doesn't have). ACAO takes one value, so it's echoed per origin; @@ -155,9 +155,9 @@ api.muchq.com { # client sees and the route the model declares are one string. reverse_proxy @post_v2_analyze one_d4_v2:8090 - # Same pattern (#1359): r3dr_v2 serves /r3dr/v2/* unrewritten. - reverse_proxy @post_r3dr_shorten r3dr_v2:8091 - reverse_proxy @get_r3dr_redirect r3dr_v2:8091 + # Same pattern (#1359): iili serves /iili/v1/* unrewritten. + reverse_proxy @post_iili_shorten iili:8091 + reverse_proxy @get_iili_redirect iili:8091 log { output file /var/log/caddy/access.log { @@ -168,20 +168,17 @@ api.muchq.com { } } -# Short-link redirects for iili.uk (#1359). SPA stays on Cloudflare at -# iili.uk; this host (A-record to the consolidated box) only answers -# /r/{slug}. Public path is rewritten onto r3dr_v2's modeled -# /r3dr/v2/r/{slug} — the deliberate exception to "gateway == model path" -# on api.muchq.com. HEAD is forwarded with GET for unfurlers; end-to-end -# 302 on HEAD waits on #1433 (smithy registers GET only today → 405). +# Short-link redirects for iili.uk (#1359); the SPA is on Cloudflare at +# iili.uk. The public /r/{slug} is rewritten onto iili's modeled +# /iili/v1/r/{slug} — the deliberate exception to "gateway == model path". i.iili.uk { @getredir { method GET HEAD path /r/* } handle @getredir { - rewrite * /r3dr/v2{path} - reverse_proxy r3dr_v2:8091 + rewrite * /iili/v1{path} + reverse_proxy iili:8091 } respond 404 diff --git a/deploy/consolidated/README.md b/deploy/consolidated/README.md index af17cdf9e..cab23f720 100644 --- a/deploy/consolidated/README.md +++ b/deploy/consolidated/README.md @@ -19,12 +19,11 @@ individually targetable. - [`microgpt-serve`](../../domains/ai/apis/microgpt_serve) (port 8087) - [`one_d4`](../../domains/games/apis/one_d4) (port 8088) - [`one_d4_v2`](../../domains/games/apis/one_d4_v2) (port 8090) - - [`r3dr_v2`](../../domains/r3dr/apis/r3dr_v2) (port 8091, `/r3dr/v2/*`) + - [`iili`](../../domains/iili/apis/iili) (port 8091, `/iili/v1/*`) - **i.iili.uk** - Short-link redirects (#1359) - - Caddy rewrites `GET|HEAD /r/{slug}` → `r3dr_v2` `/r3dr/v2/r/{slug}` + - Caddy rewrites `GET|HEAD /r/{slug}` → `iili` `/iili/v1/r/{slug}` - SPA lives on Cloudflare at `iili.uk` (not this host) - - `HEAD` end-to-end 302 tracked in #1433 - **Observability Stack** - Prometheus (port 9090) @@ -200,11 +199,12 @@ nothing from the host filesystem. ### Database URLs -golf_hub, one_d4 and r3dr_v2 take their database URLs from `compose.yaml`, interpolating a -password from the host's `~/.env`: `GOLF_HUB_DB_URL` and `R3DR_V2_DB_URL` (libpq form, read from +golf_hub, one_d4 and iili take their database URLs from `compose.yaml`, interpolating a +password from the host's `~/.env`: `GOLF_HUB_DB_URL` and `IILI_DB_URL` (libpq form, read from C++) and `INDEXER_DB_URL` (JDBC form, read by pgjdbc). All point at the `shared_postgres` service. -**`R3DR_V2_DB_PASSWORD` is a new secret** in `~/.env` (#1359); `r3dr_v2_db_init` provisions the -`r3dr_v2` role and database with it on every deploy, idempotently. Keep it URL-safe (no +**`R3DR_V2_DB_PASSWORD`** in `~/.env` (#1359) is the one place the old name survives: `iili_db_init` +provisions the `r3dr_v2` role and database with it on every deploy, idempotently, because renaming +a role and database holding live rows is an operation, not a rename. Keep it URL-safe (no `@ / ? # %` or quotes): it rides in a libpq URL and a single-quoted SQL literal. Compose refuses to start the service if it's unset. @@ -237,8 +237,8 @@ All services run on the `muchq_network` Docker bridge network. ## The shared database `shared_postgres` is the one Postgres instance on the host. `one_d4`, -`golf_hub` and `r3dr_v2` each keep a database on it; `golf_hub_db_init` and -`r3dr_v2_db_init` provision their roles and databases on every deploy +`golf_hub` and `iili` each keep a database on it; `golf_hub_db_init` and +`iili_db_init` provision their roles and databases on every deploy (idempotently — the `docker-entrypoint-initdb.d` hook only fires on a fresh volume). diff --git a/deploy/consolidated/compose.yaml b/deploy/consolidated/compose.yaml index 1beedbab9..ce015131e 100644 --- a/deploy/consolidated/compose.yaml +++ b/deploy/consolidated/compose.yaml @@ -19,7 +19,7 @@ services: - one_d4_v2 - portrait - prom_proxy - - r3dr_v2 + - iili - mcpserver networks: # Static address so services can name Caddy — and only Caddy — as @@ -244,8 +244,8 @@ services: retries: 3 start_period: 10s - r3dr_v2: - image: ghcr.io/muchq/r3dr_v2:${R3DR_V2_SHA:-${DEPLOY_SHA:-latest}} + iili: + image: ghcr.io/muchq/iili:${IILI_SHA:-${DEPLOY_SHA:-latest}} labels: com.muchq.description: "URL shortener v2 API (C++)" restart: always @@ -253,15 +253,15 @@ services: - "8091:8091" environment: PORT: "8091" - R3DR_V2_DB_URL: postgresql://r3dr_v2:${R3DR_V2_DB_PASSWORD:?unset}@shared_postgres:5432/r3dr_v2 + IILI_DB_URL: postgresql://r3dr_v2:${R3DR_V2_DB_PASSWORD:?unset}@shared_postgres:5432/r3dr_v2 TRUSTED_PROXY_CIDRS: *caddy_ip OTEL_EXPORTER_OTLP_ENDPOINT: http://otelcol:4318 - OTEL_SERVICE_NAME: r3dr_v2 - OTEL_RESOURCE_ATTRIBUTES: service.name=r3dr_v2,service.version=${R3DR_V2_SHA:-${DEPLOY_SHA:-latest}} + OTEL_SERVICE_NAME: iili + OTEL_RESOURCE_ATTRIBUTES: service.name=iili,service.version=${IILI_SHA:-${DEPLOY_SHA:-latest}} networks: - app_network depends_on: - r3dr_v2_db_init: + iili_db_init: condition: service_completed_successfully deploy: resources: @@ -373,10 +373,10 @@ services: shared_postgres: condition: service_healthy - # Idempotent one-shot provisioning of r3dr_v2's database and role - # (#1359), mirroring golf_hub_db_init. Deliberately not named r3dr: the - # Go service's storage must stay un-aliasable from this file. - r3dr_v2_db_init: + # The role, database and password keep the r3dr_v2 names the live rows were + # provisioned under: renaming those is a live-database operation, not a + # rename of this file. + iili_db_init: image: postgres:18 restart: "no" environment: diff --git a/deploy/consolidated/deploy_config_test.go b/deploy/consolidated/deploy_config_test.go index 094a6b332..74b5700d0 100644 --- a/deploy/consolidated/deploy_config_test.go +++ b/deploy/consolidated/deploy_config_test.go @@ -105,8 +105,8 @@ var servicesWithSteadyProbes = []string{ "one_d4", "one_d4_v2", "portrait", + "iili", "posterize", - "r3dr_v2", } func TestEveryServiceOnTheStandardRailsIsProbed(t *testing.T) { @@ -673,7 +673,7 @@ func TestTheMcpCorsAllowListCarriesTheProtocolHeaders(t *testing.T) { } } -// api.muchq.com is called from browsers on muchq.com (incl. /r3dr) and on +// api.muchq.com is called from browsers on muchq.com (incl. /iili) and on // iili.uk (the iili_web worker SPA; the localhost origin is vite dev). // ACAO takes a single value, so the Caddyfile echoes it per allowed origin // via named matchers — on ordinary responses and on the preflight handler @@ -1022,9 +1022,9 @@ var publicRoutes = []struct { // one_d4_v2 (#1389 phase 6): mcpserver reaches it directly over the // Compose network, but the public route is served through Caddy. {"@post_v2_analyze", []string{"method POST", "path /v2/analyze"}, "one_d4_v2:8090"}, - // r3dr_v2 (#1359): the redirect matcher is the product. - {"@post_r3dr_shorten", []string{"method POST", "path /r3dr/v2/shorten"}, "r3dr_v2:8091"}, - {"@get_r3dr_redirect", []string{"method GET", "path /r3dr/v2/r/*"}, "r3dr_v2:8091"}, + // iili (#1359): the redirect matcher is the product. + {"@post_iili_shorten", []string{"method POST", "path /iili/v1/shorten"}, "iili:8091"}, + {"@get_iili_redirect", []string{"method GET", "path /iili/v1/r/*"}, "iili:8091"}, } func TestPublicRoutesAreDeliberatelyExact(t *testing.T) { @@ -1077,10 +1077,9 @@ func TestPublicRoutesAreDeliberatelyExact(t *testing.T) { // i.iili.uk is the short-link redirect host (#1359): A-record to the // consolidated box, SPA stays on Cloudflare at iili.uk. Public path is -// /r/{slug}; r3dr_v2's modeled path is /r3dr/v2/r/{slug}, so this site -// rewrites — the deliberate exception to "gateway path == model path" -// on api.muchq.com. HEAD is forwarded with GET (#1433 pins end-to-end 302). -func TestIiliRedirectHostRewritesSlugPathsToR3drV2(t *testing.T) { +// /r/{slug}; iili's modeled path is /iili/v1/r/{slug}, so this site +// rewrites — the deliberate exception to "gateway path == model path". +func TestIiliRedirectHostRewritesSlugPathsToIili(t *testing.T) { site := caddySiteBlock(t, "Caddyfile", "i.iili.uk") matcher := "" @@ -1092,10 +1091,10 @@ func TestIiliRedirectHostRewritesSlugPathsToR3drV2(t *testing.T) { rewrites := false proxies := false for _, inner := range body { - if strings.HasPrefix(inner, "rewrite ") && strings.Contains(inner, "/r3dr/v2") { + if strings.HasPrefix(inner, "rewrite ") && strings.Contains(inner, "/iili/v1") { rewrites = true } - if strings.HasPrefix(inner, "reverse_proxy r3dr_v2:8091") { + if strings.HasPrefix(inner, "reverse_proxy iili:8091") { proxies = true } } @@ -1105,7 +1104,7 @@ func TestIiliRedirectHostRewritesSlugPathsToR3drV2(t *testing.T) { } } if matcher == "" { - t.Fatalf("no `handle @… { rewrite …/r3dr/v2…; reverse_proxy r3dr_v2:8091 }` in the "+ + t.Fatalf("no `handle @… { rewrite …/iili/v1…; reverse_proxy iili:8091 }` in the "+ "i.iili.uk block; short links would 404 or hit the wrong upstream. Block was:\n%s", strings.Join(site, "\n")) } @@ -1138,35 +1137,18 @@ func TestIiliRedirectHostRewritesSlugPathsToR3drV2(t *testing.T) { } } -// Contains, not equality: the host also returns on a multi-host or -// scheme-prefixed address line. -func TestR3drNetCaddySitesAreGone(t *testing.T) { - for _, host := range []string{"r3dr.net", "www.r3dr.net"} { - for _, line := range directiveLines(t, "Caddyfile") { - if strings.Contains(line, host) { - t.Errorf("Caddyfile still names %q (%q); short links live on "+ - "i.iili.uk now and the SPA on Cloudflare at iili.uk.", host, line) - } - } - } -} - -// r3dr_v2 shares the prefix and would trip most of these tokens, so it is -// rewritten away before each line is checked. -func TestNoDeployConfigNamesTheGoR3drStack(t *testing.T) { - if _, ok := composeServiceLines(t, "compose.yaml")["r3dr"]; ok { - t.Error("compose.yaml declares an `r3dr` service; r3dr_v2 is the shortener") - } - forbidden := []string{"ghcr.io/muchq/r3dr:", "/var/www/r3dr", "r3dr-assets", "r3dr_web", "/etc/r3dr"} - files := []string{"compose.yaml", "Caddyfile", "Caddyfile.local", "deploy.sh", "local_deploy.sh", "initialize_host.sh"} +// Only the r3dr_v2 database role, database and password keep the old name +// (see compose.yaml); any other r3dr in the deploy surface is the retired Go +// stack or a missed rename. +func TestNoDeployConfigNamesR3dr(t *testing.T) { + dbIdentifier := regexp.MustCompile(`(?i)r3dr_v2`) + files := []string{"compose.yaml", "Caddyfile", "Caddyfile.local", "deploy.sh", + "local_deploy.sh", "initialize_host.sh"} for _, name := range files { for i, line := range strings.Split(readConfig(t, name), "\n") { - sansV2 := strings.ReplaceAll(line, "r3dr_v2", "V2") - for _, token := range forbidden { - if strings.Contains(sansV2, token) { - t.Errorf("%s:%d names %q (%q); r3dr_v2 is the shortener", - name, i+1, token, strings.TrimSpace(line)) - } + if strings.Contains(strings.ToLower(dbIdentifier.ReplaceAllString(line, "")), "r3dr") { + t.Errorf("%s:%d names r3dr (%q); the shortener is iili", name, i+1, + strings.TrimSpace(line)) } } } diff --git a/domains/iili/README.md b/domains/iili/README.md new file mode 100644 index 000000000..bc126e456 --- /dev/null +++ b/domains/iili/README.md @@ -0,0 +1,15 @@ +# iili Domain + +URL shortening services and related applications. + +## APIs + +- [**iili API**](apis/iili): The C++ shortener on smithy-cpp (#1359), + serving `/iili/v1/*` behind `api.muchq.com`. + +## Apps + +- [**iili web**](apps/iili_web): The standalone frontend (#1359) — a + Cloudflare Worker SPA at `iili.uk`. Short links mint as + `i.iili.uk/r/{slug}` (Caddy on the consolidated host → iili). A + second frontend lives at muchq.com/iili (the muchq.github.io repo). diff --git a/domains/r3dr/apis/r3dr_v2/BUILD.bazel b/domains/iili/apis/iili/BUILD.bazel similarity index 90% rename from domains/r3dr/apis/r3dr_v2/BUILD.bazel rename to domains/iili/apis/iili/BUILD.bazel index c221b2f3a..b0bccc676 100644 --- a/domains/r3dr/apis/r3dr_v2/BUILD.bazel +++ b/domains/iili/apis/iili/BUILD.bazel @@ -2,7 +2,7 @@ load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") load("@smithy_cpp//bazel:defs.bzl", "smithy_cpp_client_library", "smithy_cpp_server_library") load("//bazel/rules:oci.bzl", "linux_amd64_oci_binary") -# r3dr_v2 (#1359): the C++ URL shortener serving /r3dr/v2/* on +# iili (#1359): the C++ URL shortener serving /iili/v1/* on # api.muchq.com, beside the Go service until it retires. cc_library( @@ -116,23 +116,23 @@ cc_test( ) smithy_cpp_server_library( - name = "r3dr_v2_smithy_server", + name = "iili_smithy_server", srcs = [ "model/bindings/simplerestjson.smithy", - "model/r3dr_v2.smithy", + "model/iili.smithy", ], - namespace = "moonbase::r3dr", - service = "moonbase.r3dr#R3drV2", + namespace = "moonbase::iili", + service = "moonbase.iili#Iili", ) smithy_cpp_client_library( - name = "r3dr_v2_smithy_client", + name = "iili_smithy_client", srcs = [ "model/bindings/simplerestjson.smithy", - "model/r3dr_v2.smithy", + "model/iili.smithy", ], - namespace = "moonbase::r3dr", - service = "moonbase.r3dr#R3drV2", + namespace = "moonbase::iili", + service = "moonbase.iili#Iili", ) cc_library( @@ -141,7 +141,7 @@ cc_library( hdrs = ["smithy_handler.h"], visibility = ["//visibility:public"], deps = [ - ":r3dr_v2_smithy_server", + ":iili_smithy_server", ":shortener", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", @@ -167,8 +167,8 @@ cc_test( size = "small", srcs = ["wire_test.cc"], deps = [ - ":r3dr_v2_smithy_client", - ":r3dr_v2_smithy_server", + ":iili_smithy_client", + ":iili_smithy_server", ":shortener", ":smithy_handler", ":test_support", @@ -182,12 +182,12 @@ cc_test( ) cc_binary( - name = "r3dr_v2", + name = "iili", srcs = ["main.cc"], visibility = ["//visibility:public"], deps = [ + ":iili_smithy_server", ":pg_url_store", - ":r3dr_v2_smithy_server", ":shortener", ":smithy_handler", "//domains/platform/libs/aura", @@ -208,14 +208,14 @@ cc_binary( ], ) -linux_amd64_oci_binary(bin_name = "r3dr_v2") +linux_amd64_oci_binary(bin_name = "iili") cc_test( name = "production_chain_test", size = "small", srcs = ["production_chain_test.cc"], deps = [ - ":r3dr_v2_smithy_server", + ":iili_smithy_server", ":shortener", ":smithy_handler", ":test_support", diff --git a/domains/r3dr/apis/r3dr_v2/README.md b/domains/iili/apis/iili/README.md similarity index 66% rename from domains/r3dr/apis/r3dr_v2/README.md rename to domains/iili/apis/iili/README.md index cafb58cfe..12d34b97c 100644 --- a/domains/r3dr/apis/r3dr_v2/README.md +++ b/domains/iili/apis/iili/README.md @@ -1,20 +1,20 @@ -# r3dr_v2 +# iili -The C++ URL shortener (#1359). Serves `/r3dr/v2/*` behind `api.muchq.com`; +The C++ URL shortener (#1359). Serves `/iili/v1/*` behind `api.muchq.com`; short links resolve on `i.iili.uk`, which Caddy rewrites onto that path. ## Routes -- `POST /r3dr/v2/shorten` → 201 `{"slug":"..."}`. `longUrl`: 11–1000 chars, +- `POST /iili/v1/shorten` → 201 `{"slug":"..."}`. `longUrl`: 11–1000 chars, `http://`/`https://`, trait-validated. `expiresAt` (epoch millis) required: in the future, ceiling 30d. -- `GET /r3dr/v2/r/{slug}` → 302 with `Location`. Expiry enforced in the SQL +- `GET /iili/v1/r/{slug}` → 302 with `Location`. Expiry enforced in the SQL and in the cache entry. Unknown, expired, or non-slug-shaped: one modeled JSON 404. Store failure: 500, not 404. - `/health` via aura. The API returns a bare slug; [`iili_web`](../../apps/iili_web) fronts it as `https://i.iili.uk/r/{slug}` (SPA at iili.uk on Cloudflare; redirects on -Caddy at i.iili.uk), and muchq.com/r3dr mints the same links. CORS for +Caddy at i.iili.uk), and muchq.com/iili mints the same links. CORS for `muchq.com` and `iili.uk` is set at the api.muchq.com Caddy block. Error shapes: 404s and clock-rule 400s are modeled JSON (`{"message":...}`); @@ -30,10 +30,11 @@ invalidation; the per-client rate limit is the scan defense). ## Storage -Own `r3dr_v2` database/role on `shared_postgres`, provisioned by the -`r3dr_v2_db_init` one-shot from `R3DR_V2_DB_PASSWORD` — a new `~/.env` -secret; compose refuses to start without it. Schema applied at startup by `migrations.cc` -(idempotent, fail-fast) and in every DB test's SetUp. Two `pg::Client`s so +Own database and role on `shared_postgres`, both still named `r3dr_v2` and +provisioned by the `iili_db_init` one-shot from `R3DR_V2_DB_PASSWORD` in +`~/.env` — renaming a role and database holding live rows is an operation, +not a rename. Compose refuses to start without the secret. Schema applied at +startup by `migrations.cc` (idempotent, fail-fast) and in every DB test's SetUp. Two `pg::Client`s so redirects don't queue behind shortens. Inserts are `ON CONFLICT (id) DO UPDATE … RETURNING (xmax = 0)`: a conflict row identical to ours is the client's reconnect-retry replaying (success, same slug); any other row means @@ -43,21 +44,21 @@ refuses rather than alias a slug. ## Run it ```bash -R3DR_V2_DB_URL=postgresql://user:pass@localhost:5432/r3dr_v2 \ - bazel run //domains/r3dr/apis/r3dr_v2 -curl localhost:8091/r3dr/v2/shorten -H 'content-type: application/json' \ +IILI_DB_URL=postgresql://user:pass@localhost:5432/r3dr_v2 \ + bazel run //domains/iili/apis/iili +curl localhost:8091/iili/v1/shorten -H 'content-type: application/json' \ -d "{\"longUrl\":\"https://example.com/some/where\",\"expiresAt\":$(( ($(date +%s) + 3600) * 1000 ))}" -curl -i localhost:8091/r3dr/v2/r/AQA +curl -i localhost:8091/iili/v1/r/AQA ``` -`bazel test //domains/r3dr/apis/r3dr_v2/...` — `pg_url_store_test` skips +`bazel test //domains/iili/apis/iili/...` — `pg_url_store_test` skips silently without `PG_TEST_DB_URL` (CI supplies it). ## Deployment On `api.muchq.com`, Caddy proxies the two operation routes unrewritten. On `i.iili.uk`, Caddy rewrites `GET|HEAD /r/{slug}` onto -`/r3dr/v2/r/{slug}` before the same upstream. `TRUSTED_PROXY_CIDRS` +`/iili/v1/r/{slug}` before the same upstream. `TRUSTED_PROXY_CIDRS` anchors the per-client limit (120/min, bounded keys) to real client addresses. 0.25 CPU / 256M. Redeploys are safe: boot migrations are idempotent. diff --git a/domains/r3dr/apis/r3dr_v2/encoding.cc b/domains/iili/apis/iili/encoding.cc similarity index 91% rename from domains/r3dr/apis/r3dr_v2/encoding.cc rename to domains/iili/apis/iili/encoding.cc index 025d1c945..df4963b0b 100644 --- a/domains/r3dr/apis/r3dr_v2/encoding.cc +++ b/domains/iili/apis/iili/encoding.cc @@ -1,4 +1,4 @@ -#include "domains/r3dr/apis/r3dr_v2/encoding.h" +#include "domains/iili/apis/iili/encoding.h" #include #include @@ -7,7 +7,7 @@ #include "absl/status/status.h" #include "absl/strings/escaping.h" -namespace r3dr_v2 { +namespace iili { namespace { std::string LittleEndianBytes(uint64_t value, int width) { @@ -40,4 +40,4 @@ bool IsPossibleSlug(std::string_view slug) { return slug.size() == 3 || slug.size() == 6 || slug.size() == 11; } -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/encoding.h b/domains/iili/apis/iili/encoding.h similarity index 77% rename from domains/r3dr/apis/r3dr_v2/encoding.h rename to domains/iili/apis/iili/encoding.h index 9a8502323..1202a4e73 100644 --- a/domains/r3dr/apis/r3dr_v2/encoding.h +++ b/domains/iili/apis/iili/encoding.h @@ -1,5 +1,5 @@ -#ifndef DOMAINS_R3DR_APIS_R3DR_V2_ENCODING_H -#define DOMAINS_R3DR_APIS_R3DR_V2_ENCODING_H +#ifndef DOMAINS_IILI_APIS_IILI_ENCODING_H +#define DOMAINS_IILI_APIS_IILI_ENCODING_H #include #include @@ -7,7 +7,7 @@ #include "absl/status/statusor.h" -namespace r3dr_v2 { +namespace iili { /// Id -> slug: little-endian bytes (2/4/8, stepping at MaxInt16/MaxInt32), /// base64url unpadded — so slugs are exactly 3, 6, or 11 chars and widths @@ -20,6 +20,6 @@ absl::StatusOr EncodeId(int64_t id); /// the store. bool IsPossibleSlug(std::string_view slug); -} // namespace r3dr_v2 +} // namespace iili -#endif // DOMAINS_R3DR_APIS_R3DR_V2_ENCODING_H +#endif // DOMAINS_IILI_APIS_IILI_ENCODING_H diff --git a/domains/r3dr/apis/r3dr_v2/encoding_test.cc b/domains/iili/apis/iili/encoding_test.cc similarity index 96% rename from domains/r3dr/apis/r3dr_v2/encoding_test.cc rename to domains/iili/apis/iili/encoding_test.cc index 65bda504a..825868815 100644 --- a/domains/r3dr/apis/r3dr_v2/encoding_test.cc +++ b/domains/iili/apis/iili/encoding_test.cc @@ -1,4 +1,4 @@ -#include "domains/r3dr/apis/r3dr_v2/encoding.h" +#include "domains/iili/apis/iili/encoding.h" #include @@ -8,7 +8,7 @@ #include #include -namespace r3dr_v2 { +namespace iili { namespace { // Byte order, widths, and alphabet pinned at the transitions. Five vectors @@ -68,4 +68,4 @@ TEST(EncodingTest, ANegativeIdIsRefused) { } } // namespace -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/main.cc b/domains/iili/apis/iili/main.cc similarity index 76% rename from domains/r3dr/apis/r3dr_v2/main.cc rename to domains/iili/apis/iili/main.cc index e03c877a4..9d4fdc964 100644 --- a/domains/r3dr/apis/r3dr_v2/main.cc +++ b/domains/iili/apis/iili/main.cc @@ -1,8 +1,8 @@ -// The r3dr_v2 server (#1359): the generated R3drV2 API on the Beast +// The iili server (#1359): the generated Iili API on the Beast // transport, with observability, health, and per-client rate limiting. // -// R3DR_V2_DB_URL=postgresql://... bazel run //domains/r3dr/apis/r3dr_v2 -// curl localhost:8091/r3dr/v2/shorten -H 'content-type: application/json' \ +// IILI_DB_URL=postgresql://... bazel run //domains/iili/apis/iili +// curl localhost:8091/iili/v1/shorten -H 'content-type: application/json' \ // -d '{"longUrl":"https://example.com/some/where"}' #include @@ -19,6 +19,10 @@ #include "absl/log/initialize.h" #include "absl/log/log.h" #include "absl/time/clock.h" +#include "domains/iili/apis/iili/migrations.h" +#include "domains/iili/apis/iili/pg_url_store.h" +#include "domains/iili/apis/iili/shortener.h" +#include "domains/iili/apis/iili/smithy_handler.h" #include "domains/platform/libs/aura/cache.h" #include "domains/platform/libs/aura/middleware.h" #include "domains/platform/libs/futility/env/env.h" @@ -27,25 +31,21 @@ #include "domains/platform/libs/futility/otel/otel_provider.h" #include "domains/platform/libs/futility/rate_limiter/sliding_window_rate_limiter.h" #include "domains/platform/libs/pg/pg.h" -#include "domains/r3dr/apis/r3dr_v2/migrations.h" -#include "domains/r3dr/apis/r3dr_v2/pg_url_store.h" -#include "domains/r3dr/apis/r3dr_v2/shortener.h" -#include "domains/r3dr/apis/r3dr_v2/smithy_handler.h" -#include "moonbase/r3dr/server.h" +#include "moonbase/iili/server.h" #include "smithy/http/beast_transport.h" int main() { absl::InitializeLog(); absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); - futility::otel::OtelConfig otel_config{.service_name = "r3dr_v2", .service_version = "1.0.0"}; + futility::otel::OtelConfig otel_config{.service_name = "iili", .service_version = "1.0.0"}; futility::otel::OtelProvider otel_provider(otel_config); // No fallback: anything this could start on unattended loses links // silently on restart. - const char* db_url = std::getenv("R3DR_V2_DB_URL"); + const char* db_url = std::getenv("IILI_DB_URL"); if (db_url == nullptr || *db_url == '\0') { - LOG(ERROR) << "R3DR_V2_DB_URL is not set. r3dr_v2 needs a libpq URL " + LOG(ERROR) << "IILI_DB_URL is not set. iili needs a libpq URL " << "(postgresql://user:pass@host:5432/db); there is no in-memory fallback."; return 1; } @@ -53,22 +53,22 @@ int main() { // Two clients so redirects never queue behind shortens. auto read_db = std::make_shared(db_url); auto write_db = std::make_shared(db_url); - if (absl::Status migrated = r3dr_v2::RunMigrations(*write_db); !migrated.ok()) { + if (absl::Status migrated = iili::RunMigrations(*write_db); !migrated.ok()) { LOG(ERROR) << "Migrations failed; refusing to serve against an unknown schema: " << migrated; return 1; } - auto metrics_recorder = std::make_shared("r3dr_v2"); - auto shortener = std::make_shared( - std::make_shared(read_db, write_db), - std::make_shared("url_cache", 1000, metrics_recorder), + auto metrics_recorder = std::make_shared("iili"); + auto shortener = std::make_shared( + std::make_shared(read_db, write_db), + std::make_shared("url_cache", 1000, metrics_recorder), [] { return absl::Now(); }); - moonbase::r3dr::R3drV2Server server( - std::make_shared(std::move(shortener))); + moonbase::iili::IiliServer server( + std::make_shared(std::move(shortener))); auto metrics = - aura::MakeHttpMetricsSink(std::make_shared("r3dr_v2")); + aura::MakeHttpMetricsSink(std::make_shared("iili")); // Generous because redirects burst; still per client, keys bounded. futility::rate_limiter::SlidingWindowRateLimiterConfig limiter_config{ @@ -91,7 +91,7 @@ int main() { [rate_limiter](const std::string& client) { return rate_limiter->allow(client); }, .trusted_proxies = std::move(*trusted_proxies), .retry_after = std::chrono::seconds(60)}, - server.Handler()); + iili::WithHeadAsGet(server.Handler())); // Block shutdown signals before the transport spawns its thread pool — // and only now, so a hung DB connection during boot is still killable. @@ -117,11 +117,11 @@ int main() { return 1; } - LOG(INFO) << "r3dr_v2 running on http://" << options.address << ":" << transport.port(); + LOG(INFO) << "iili running on http://" << options.address << ":" << transport.port(); LOG(INFO) << "Serving:"; LOG(INFO) << " GET http://localhost:" << transport.port() << "/health"; - LOG(INFO) << " POST http://localhost:" << transport.port() << "/r3dr/v2/shorten"; - LOG(INFO) << " GET http://localhost:" << transport.port() << "/r3dr/v2/r/{slug}"; + LOG(INFO) << " POST http://localhost:" << transport.port() << "/iili/v1/shorten"; + LOG(INFO) << " GET http://localhost:" << transport.port() << "/iili/v1/r/{slug}"; int signal_number = 0; sigwait(&shutdown_signals, &signal_number); diff --git a/domains/r3dr/apis/r3dr_v2/migrations.cc b/domains/iili/apis/iili/migrations.cc similarity index 94% rename from domains/r3dr/apis/r3dr_v2/migrations.cc rename to domains/iili/apis/iili/migrations.cc index 1dd791426..c25277ac4 100644 --- a/domains/r3dr/apis/r3dr_v2/migrations.cc +++ b/domains/iili/apis/iili/migrations.cc @@ -1,6 +1,6 @@ -#include "domains/r3dr/apis/r3dr_v2/migrations.h" +#include "domains/iili/apis/iili/migrations.h" -namespace r3dr_v2 { +namespace iili { absl::Status RunMigrations(pg::Client& db) { // url_ids is standalone (not an identity column) because the slug is @@ -34,4 +34,4 @@ absl::Status RunMigrations(pg::Client& db) { return absl::OkStatus(); } -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/migrations.h b/domains/iili/apis/iili/migrations.h similarity index 61% rename from domains/r3dr/apis/r3dr_v2/migrations.h rename to domains/iili/apis/iili/migrations.h index 21a55be5e..44ffa491e 100644 --- a/domains/r3dr/apis/r3dr_v2/migrations.h +++ b/domains/iili/apis/iili/migrations.h @@ -1,16 +1,16 @@ -#ifndef DOMAINS_R3DR_APIS_R3DR_V2_MIGRATIONS_H -#define DOMAINS_R3DR_APIS_R3DR_V2_MIGRATIONS_H +#ifndef DOMAINS_IILI_APIS_IILI_MIGRATIONS_H +#define DOMAINS_IILI_APIS_IILI_MIGRATIONS_H #include "absl/status/status.h" #include "domains/platform/libs/pg/pg.h" -namespace r3dr_v2 { +namespace iili { /// Idempotent statements run at startup (fail-fast) and in every DB test's /// SetUp. Schema changes append in migrations.cc; every statement must stay /// safe to re-run. absl::Status RunMigrations(pg::Client& db); -} // namespace r3dr_v2 +} // namespace iili -#endif // DOMAINS_R3DR_APIS_R3DR_V2_MIGRATIONS_H +#endif // DOMAINS_IILI_APIS_IILI_MIGRATIONS_H diff --git a/domains/iili/apis/iili/model/bindings/simplerestjson.smithy b/domains/iili/apis/iili/model/bindings/simplerestjson.smithy new file mode 100644 index 000000000..2292105bd --- /dev/null +++ b/domains/iili/apis/iili/model/bindings/simplerestjson.smithy @@ -0,0 +1,10 @@ +// Protocol binding overlay: binds the protocol-agnostic Iili service to +// alloy#simpleRestJson (plain REST/JSON). Passed alongside model/iili.smithy +// to the generation rules. +$version: "2.0" + +namespace moonbase.iili + +use alloy#simpleRestJson + +apply Iili @simpleRestJson diff --git a/domains/r3dr/apis/r3dr_v2/model/r3dr_v2.smithy b/domains/iili/apis/iili/model/iili.smithy similarity index 74% rename from domains/r3dr/apis/r3dr_v2/model/r3dr_v2.smithy rename to domains/iili/apis/iili/model/iili.smithy index 4b41782d2..1c6359c1e 100644 --- a/domains/r3dr/apis/r3dr_v2/model/r3dr_v2.smithy +++ b/domains/iili/apis/iili/model/iili.smithy @@ -1,21 +1,18 @@ $version: "2.0" -namespace moonbase.r3dr +namespace moonbase.iili -/// r3dr v2 (#1359): mint a slug for a long URL, redirect a slug back to it. -/// Serves /r3dr/v2/* itself — the gateway path and the modeled path are one -/// string (no Caddy rewrite). /r3dr/v2/* is the public HTTP contract; -/// _v2 names the implementation generation. longUrl is -/// trait-validated here; the clock-dependent expiry rules answer as -/// InvalidRequestError from the service. -service R3drV2 { +/// iili (#1359): mint a slug for a long URL, redirect a slug back to it. +/// longUrl is trait-validated here; the clock-dependent expiry rules answer +/// as InvalidRequestError from the service. +service Iili { version: "2026-08-21" operations: [Shorten, Redirect] } /// 201 with the slug alone: the caller builds the short link, so the /// link's domain is a client constant. -@http(method: "POST", uri: "/r3dr/v2/shorten", code: 201) +@http(method: "POST", uri: "/iili/v1/shorten", code: 201) operation Shorten { input := { @required @@ -43,7 +40,7 @@ operation Shorten { /// wire_test pins ours. @readonly @suppress(["HttpResponseCodeSemantics"]) -@http(method: "GET", uri: "/r3dr/v2/r/{slug}", code: 302) +@http(method: "GET", uri: "/iili/v1/r/{slug}", code: 302) operation Redirect { input := { @required diff --git a/domains/r3dr/apis/r3dr_v2/pg_url_store.cc b/domains/iili/apis/iili/pg_url_store.cc similarity index 95% rename from domains/r3dr/apis/r3dr_v2/pg_url_store.cc rename to domains/iili/apis/iili/pg_url_store.cc index 87dee434e..95cd0fa2f 100644 --- a/domains/r3dr/apis/r3dr_v2/pg_url_store.cc +++ b/domains/iili/apis/iili/pg_url_store.cc @@ -1,4 +1,4 @@ -#include "domains/r3dr/apis/r3dr_v2/pg_url_store.h" +#include "domains/iili/apis/iili/pg_url_store.h" #include #include @@ -7,9 +7,9 @@ #include "absl/status/status.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" -#include "domains/r3dr/apis/r3dr_v2/encoding.h" +#include "domains/iili/apis/iili/encoding.h" -namespace r3dr_v2 { +namespace iili { namespace { // $1 = id, $2 = slug, $3 = long url, $4 = expiry epoch millis. One row @@ -84,4 +84,4 @@ absl::StatusOr> PgUrlStore::Lookup(const std::string& slug return std::optional(Target{*std::move(long_url), absl::FromUnixMillis(millis)}); } -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/pg_url_store.h b/domains/iili/apis/iili/pg_url_store.h similarity index 83% rename from domains/r3dr/apis/r3dr_v2/pg_url_store.h rename to domains/iili/apis/iili/pg_url_store.h index 4b548480e..91505a99c 100644 --- a/domains/r3dr/apis/r3dr_v2/pg_url_store.h +++ b/domains/iili/apis/iili/pg_url_store.h @@ -1,5 +1,5 @@ -#ifndef DOMAINS_R3DR_APIS_R3DR_V2_PG_URL_STORE_H -#define DOMAINS_R3DR_APIS_R3DR_V2_PG_URL_STORE_H +#ifndef DOMAINS_IILI_APIS_IILI_PG_URL_STORE_H +#define DOMAINS_IILI_APIS_IILI_PG_URL_STORE_H #include #include @@ -8,10 +8,10 @@ #include "absl/status/statusor.h" #include "absl/time/time.h" +#include "domains/iili/apis/iili/url_store.h" #include "domains/platform/libs/pg/pg.h" -#include "domains/r3dr/apis/r3dr_v2/url_store.h" -namespace r3dr_v2 { +namespace iili { /// The urls table. Two clients because pg::Client is one serialized /// connection — a shared one would queue every redirect behind every @@ -36,6 +36,6 @@ class PgUrlStore final : public UrlStore { std::shared_ptr writes_; }; -} // namespace r3dr_v2 +} // namespace iili -#endif // DOMAINS_R3DR_APIS_R3DR_V2_PG_URL_STORE_H +#endif // DOMAINS_IILI_APIS_IILI_PG_URL_STORE_H diff --git a/domains/r3dr/apis/r3dr_v2/pg_url_store_test.cc b/domains/iili/apis/iili/pg_url_store_test.cc similarity index 98% rename from domains/r3dr/apis/r3dr_v2/pg_url_store_test.cc rename to domains/iili/apis/iili/pg_url_store_test.cc index 69efff8b1..fe33da432 100644 --- a/domains/r3dr/apis/r3dr_v2/pg_url_store_test.cc +++ b/domains/iili/apis/iili/pg_url_store_test.cc @@ -1,4 +1,4 @@ -#include "domains/r3dr/apis/r3dr_v2/pg_url_store.h" +#include "domains/iili/apis/iili/pg_url_store.h" #include @@ -10,12 +10,12 @@ #include "absl/strings/str_cat.h" #include "absl/time/clock.h" #include "absl/time/time.h" +#include "domains/iili/apis/iili/migrations.h" #include "domains/platform/libs/pg/pg.h" -#include "domains/r3dr/apis/r3dr_v2/migrations.h" // Real SQL against real Postgres; skips without PG_TEST_DB_URL. -namespace r3dr_v2 { +namespace iili { namespace { class PgUrlStoreTest : public testing::Test { @@ -199,4 +199,4 @@ TEST_F(PgUrlStoreTest, TheCheckConstraintRefusesAnOversizedUrl) { } } // namespace -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/production_chain_test.cc b/domains/iili/apis/iili/production_chain_test.cc similarity index 94% rename from domains/r3dr/apis/r3dr_v2/production_chain_test.cc rename to domains/iili/apis/iili/production_chain_test.cc index 0cea00f7f..b7c0d110c 100644 --- a/domains/r3dr/apis/r3dr_v2/production_chain_test.cc +++ b/domains/iili/apis/iili/production_chain_test.cc @@ -15,16 +15,16 @@ #include #include "absl/time/time.h" +#include "domains/iili/apis/iili/smithy_handler.h" +#include "domains/iili/apis/iili/test_support.h" #include "domains/platform/libs/aura/middleware.h" #include "domains/platform/libs/futility/rate_limiter/sliding_window_rate_limiter.h" -#include "domains/r3dr/apis/r3dr_v2/smithy_handler.h" -#include "domains/r3dr/apis/r3dr_v2/test_support.h" -#include "moonbase/r3dr/server.h" +#include "moonbase/iili/server.h" #include "smithy/http/beast_transport.h" #include "smithy/http/loopback.h" #include "smithy/http/socket_transport.h" -namespace r3dr_v2 { +namespace iili { namespace { using futility::rate_limiter::SlidingWindowRateLimiter; @@ -71,7 +71,7 @@ class ProductionChainTest : public ::testing::Test { .allow_request = [limiter = limiter_](const std::string& client) { return limiter->allow(client); }, .retry_after = std::chrono::seconds(60)}, - server_.Handler()); + WithHeadAsGet(server_.Handler())); loopback_ = std::make_shared(); const auto started = loopback_->Start(handler_); EXPECT_TRUE(started.ok()); @@ -80,7 +80,7 @@ class ProductionChainTest : public ::testing::Test { smithy::http::HttpResponse RedirectAs(const std::string& peer) { smithy::http::HttpRequest request; request.method = "GET"; - request.target = "/r3dr/v2/r/DAA"; + request.target = "/iili/v1/r/DAA"; request.peer_address = peer; auto response = loopback_->Send(std::move(request)); EXPECT_TRUE(response.ok()); @@ -90,7 +90,7 @@ class ProductionChainTest : public ::testing::Test { std::shared_ptr sink_; std::shared_ptr> limiter_; std::shared_ptr store_; - moonbase::r3dr::R3drV2Server server_; + moonbase::iili::IiliServer server_; smithy::http::RequestHandler handler_; std::shared_ptr loopback_; }; @@ -114,7 +114,7 @@ TEST_F(ProductionChainTest, ServesRedirectHealthAnd429ThroughTheChain) { // either. A per-op split in main.cc must fail here. smithy::http::HttpRequest shorten; shorten.method = "POST"; - shorten.target = "/r3dr/v2/shorten"; + shorten.target = "/iili/v1/shorten"; shorten.peer_address = "203.0.113.4"; shorten.headers.Set("content-type", "application/json"); shorten.body = R"({"longUrl":"https://www.example.com","expiresAt":1755003600000})"; @@ -138,7 +138,7 @@ TEST_F(ProductionChainTest, ServesRedirectHealthAnd429ThroughTheChain) { TEST_F(ProductionChainTest, ShortenCarriesItsOwnRouteLabel) { smithy::http::HttpRequest request; request.method = "POST"; - request.target = "/r3dr/v2/shorten"; + request.target = "/iili/v1/shorten"; request.peer_address = "203.0.113.6"; request.headers.Set("content-type", "application/json"); request.body = R"({"longUrl":"https://www.example.com","expiresAt":1755003600000})"; @@ -167,7 +167,7 @@ TEST_F(ProductionChainTest, TheTransportAndTheUrlBoundSplitTheOversizedSpace) { smithy::http::HttpRequest big_url; big_url.method = "POST"; - big_url.target = "/r3dr/v2/shorten"; + big_url.target = "/iili/v1/shorten"; big_url.peer_address = "203.0.113.9"; big_url.headers.Set("content-type", "application/json"); big_url.body = @@ -195,4 +195,4 @@ TEST_F(ProductionChainTest, TheTransportAndTheUrlBoundSplitTheOversizedSpace) { } } // namespace -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/shortener.cc b/domains/iili/apis/iili/shortener.cc similarity index 93% rename from domains/r3dr/apis/r3dr_v2/shortener.cc rename to domains/iili/apis/iili/shortener.cc index ec5311e12..c145b7b7d 100644 --- a/domains/r3dr/apis/r3dr_v2/shortener.cc +++ b/domains/iili/apis/iili/shortener.cc @@ -1,4 +1,4 @@ -#include "domains/r3dr/apis/r3dr_v2/shortener.h" +#include "domains/iili/apis/iili/shortener.h" #include #include @@ -6,9 +6,9 @@ #include "absl/log/log.h" #include "absl/status/status.h" #include "absl/strings/str_cat.h" -#include "domains/r3dr/apis/r3dr_v2/encoding.h" +#include "domains/iili/apis/iili/encoding.h" -namespace r3dr_v2 { +namespace iili { absl::StatusOr ResolveExpiry(int64_t expires_at_millis, absl::Time now) { const absl::Time requested = absl::FromUnixMillis(expires_at_millis); @@ -61,4 +61,4 @@ absl::StatusOr> Shortener::Resolve(const std::string& return std::optional(std::move((*looked)->long_url)); } -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/shortener.h b/domains/iili/apis/iili/shortener.h similarity index 88% rename from domains/r3dr/apis/r3dr_v2/shortener.h rename to domains/iili/apis/iili/shortener.h index 357499dba..d28506b8a 100644 --- a/domains/r3dr/apis/r3dr_v2/shortener.h +++ b/domains/iili/apis/iili/shortener.h @@ -1,5 +1,5 @@ -#ifndef DOMAINS_R3DR_APIS_R3DR_V2_SHORTENER_H -#define DOMAINS_R3DR_APIS_R3DR_V2_SHORTENER_H +#ifndef DOMAINS_IILI_APIS_IILI_SHORTENER_H +#define DOMAINS_IILI_APIS_IILI_SHORTENER_H #include #include @@ -10,10 +10,10 @@ #include "absl/status/statusor.h" #include "absl/time/time.h" +#include "domains/iili/apis/iili/url_store.h" #include "domains/platform/libs/aura/cache.h" -#include "domains/r3dr/apis/r3dr_v2/url_store.h" -namespace r3dr_v2 { +namespace iili { inline constexpr absl::Duration kMaxLifetime = absl::Hours(24 * 30); @@ -49,6 +49,6 @@ class Shortener { std::function now_; }; -} // namespace r3dr_v2 +} // namespace iili -#endif // DOMAINS_R3DR_APIS_R3DR_V2_SHORTENER_H +#endif // DOMAINS_IILI_APIS_IILI_SHORTENER_H diff --git a/domains/r3dr/apis/r3dr_v2/shortener_test.cc b/domains/iili/apis/iili/shortener_test.cc similarity index 97% rename from domains/r3dr/apis/r3dr_v2/shortener_test.cc rename to domains/iili/apis/iili/shortener_test.cc index ca180f80c..3ce7c3356 100644 --- a/domains/r3dr/apis/r3dr_v2/shortener_test.cc +++ b/domains/iili/apis/iili/shortener_test.cc @@ -1,4 +1,4 @@ -#include "domains/r3dr/apis/r3dr_v2/shortener.h" +#include "domains/iili/apis/iili/shortener.h" #include @@ -10,9 +10,9 @@ #include "absl/status/status.h" #include "absl/time/time.h" -#include "domains/r3dr/apis/r3dr_v2/test_support.h" +#include "domains/iili/apis/iili/test_support.h" -namespace r3dr_v2 { +namespace iili { namespace { // --- ResolveExpiry: the clock-dependent rules, both sides of every line --- @@ -133,4 +133,4 @@ TEST_F(ShortenerTest, ARejectedExpiryNeverReachesTheStore) { } } // namespace -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/smithy_handler.cc b/domains/iili/apis/iili/smithy_handler.cc similarity index 74% rename from domains/r3dr/apis/r3dr_v2/smithy_handler.cc rename to domains/iili/apis/iili/smithy_handler.cc index c82499d13..bccf9d470 100644 --- a/domains/r3dr/apis/r3dr_v2/smithy_handler.cc +++ b/domains/iili/apis/iili/smithy_handler.cc @@ -1,11 +1,11 @@ -#include "domains/r3dr/apis/r3dr_v2/smithy_handler.h" +#include "domains/iili/apis/iili/smithy_handler.h" #include #include -namespace r3dr_v2 { +namespace iili { -namespace gen = moonbase::r3dr; +namespace gen = moonbase::iili; namespace { template @@ -50,4 +50,15 @@ smithy::Outcome SmithyShortenerHandler::Redirect( return output; } -} // namespace r3dr_v2 +smithy::http::RequestHandler WithHeadAsGet(smithy::http::RequestHandler next) { + return [next = std::move(next)](const smithy::http::HttpRequest& request) { + if (request.method != "HEAD") return next(request); + smithy::http::HttpRequest as_get = request; + as_get.method = "GET"; + smithy::http::HttpResponse response = next(as_get); + response.body.clear(); + return response; + }; +} + +} // namespace iili diff --git a/domains/iili/apis/iili/smithy_handler.h b/domains/iili/apis/iili/smithy_handler.h new file mode 100644 index 000000000..5947e01e2 --- /dev/null +++ b/domains/iili/apis/iili/smithy_handler.h @@ -0,0 +1,49 @@ +#ifndef DOMAINS_IILI_APIS_IILI_SMITHY_HANDLER_H +#define DOMAINS_IILI_APIS_IILI_SMITHY_HANDLER_H + +#include +#include + +#include "absl/status/status.h" +#include "domains/iili/apis/iili/shortener.h" +#include "moonbase/iili/server.h" +#include "smithy/core/error.h" +#include "smithy/core/outcome.h" +#include "smithy/http/transport.h" +#include "smithy/server/router.h" + +namespace iili { + +/// kInvalidArgument -> InvalidRequestError (400); everything else (the +/// store) -> unmodeled 500 with a fixed body. +smithy::Error ToSmithyError(const absl::Status& status); + +/// Type-mapping shim over Shortener, whose pieces (aura::Cache, pg::Client) +/// are themselves thread-safe — the whole story for the transport's pool. +class SmithyShortenerHandler final : public moonbase::iili::IiliHandler { + public: + explicit SmithyShortenerHandler(std::shared_ptr shortener) + : shortener_(std::move(shortener)) {} + + smithy::Outcome Shorten( + const moonbase::iili::ShortenInput& input, + const smithy::server::RequestContext& context) override; + + /// A miss is the modeled 404; a store failure is a 500, not a 404. + smithy::Outcome Redirect( + const moonbase::iili::RedirectInput& input, + const smithy::server::RequestContext& context) override; + + private: + std::shared_ptr shortener_; +}; + +/// Answers HEAD from the GET routes (#1433). The router buckets by exact +/// method, so an unmodeled HEAD 405s; unfurlers lead with it. The body is +/// dropped on the way out — content on a HEAD response desynchronizes a +/// keep-alive connection. +smithy::http::RequestHandler WithHeadAsGet(smithy::http::RequestHandler next); + +} // namespace iili + +#endif // DOMAINS_IILI_APIS_IILI_SMITHY_HANDLER_H diff --git a/domains/r3dr/apis/r3dr_v2/smithy_handler_test.cc b/domains/iili/apis/iili/smithy_handler_test.cc similarity index 75% rename from domains/r3dr/apis/r3dr_v2/smithy_handler_test.cc rename to domains/iili/apis/iili/smithy_handler_test.cc index 4b2c7e0f5..e5e8fb533 100644 --- a/domains/r3dr/apis/r3dr_v2/smithy_handler_test.cc +++ b/domains/iili/apis/iili/smithy_handler_test.cc @@ -1,11 +1,11 @@ -#include "domains/r3dr/apis/r3dr_v2/smithy_handler.h" +#include "domains/iili/apis/iili/smithy_handler.h" #include #include #include "absl/status/status.h" -namespace r3dr_v2 { +namespace iili { namespace { using ::testing::HasSubstr; @@ -13,8 +13,8 @@ using ::testing::HasSubstr; TEST(ToSmithyErrorTest, InvalidArgumentBecomesTheModeled400) { const smithy::Error error = ToSmithyError(absl::InvalidArgumentError("expiresAt is in the past")); EXPECT_EQ(error.code(), "InvalidRequestError"); - ASSERT_NE(error.detail(), nullptr); - EXPECT_EQ(error.detail()->message, + ASSERT_NE(error.detail(), nullptr); + EXPECT_EQ(error.detail()->message, "expiresAt is in the past"); } @@ -25,9 +25,9 @@ TEST(ToSmithyErrorTest, AnythingElseIsUnmodeled) { ToSmithyError(absl::UnavailableError("postgres connect failed: host shared_postgres")); EXPECT_NE(error.code(), "InvalidRequestError"); EXPECT_NE(error.code(), "NotFoundError"); - EXPECT_EQ(error.detail(), nullptr); + EXPECT_EQ(error.detail(), nullptr); EXPECT_THAT(error.message(), HasSubstr("postgres connect failed")); } } // namespace -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apis/r3dr_v2/test_support.h b/domains/iili/apis/iili/test_support.h similarity index 84% rename from domains/r3dr/apis/r3dr_v2/test_support.h rename to domains/iili/apis/iili/test_support.h index 8d78a830f..ed27f671f 100644 --- a/domains/r3dr/apis/r3dr_v2/test_support.h +++ b/domains/iili/apis/iili/test_support.h @@ -1,5 +1,5 @@ -#ifndef DOMAINS_R3DR_APIS_R3DR_V2_TEST_SUPPORT_H -#define DOMAINS_R3DR_APIS_R3DR_V2_TEST_SUPPORT_H +#ifndef DOMAINS_IILI_APIS_IILI_TEST_SUPPORT_H +#define DOMAINS_IILI_APIS_IILI_TEST_SUPPORT_H #include #include @@ -11,11 +11,11 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/time/time.h" +#include "domains/iili/apis/iili/shortener.h" +#include "domains/iili/apis/iili/url_store.h" #include "domains/platform/libs/futility/otel/capturing_metrics_recorder.h" -#include "domains/r3dr/apis/r3dr_v2/shortener.h" -#include "domains/r3dr/apis/r3dr_v2/url_store.h" -namespace r3dr_v2 { +namespace iili { inline constexpr absl::Time kNow = absl::FromUnixMillis(1755000000000); @@ -56,10 +56,10 @@ inline std::shared_ptr MakeShortener(std::shared_ptr store, std::move(store), std::make_shared( "url_cache", 100, - std::make_shared("r3dr_v2_test")), + std::make_shared("iili_test")), std::move(now)); } -} // namespace r3dr_v2 +} // namespace iili -#endif // DOMAINS_R3DR_APIS_R3DR_V2_TEST_SUPPORT_H +#endif // DOMAINS_IILI_APIS_IILI_TEST_SUPPORT_H diff --git a/domains/r3dr/apis/r3dr_v2/url_store.h b/domains/iili/apis/iili/url_store.h similarity index 81% rename from domains/r3dr/apis/r3dr_v2/url_store.h rename to domains/iili/apis/iili/url_store.h index a3c819eee..6d2a74fc9 100644 --- a/domains/r3dr/apis/r3dr_v2/url_store.h +++ b/domains/iili/apis/iili/url_store.h @@ -1,5 +1,5 @@ -#ifndef DOMAINS_R3DR_APIS_R3DR_V2_URL_STORE_H -#define DOMAINS_R3DR_APIS_R3DR_V2_URL_STORE_H +#ifndef DOMAINS_IILI_APIS_IILI_URL_STORE_H +#define DOMAINS_IILI_APIS_IILI_URL_STORE_H #include #include @@ -7,7 +7,7 @@ #include "absl/status/statusor.h" #include "absl/time/time.h" -namespace r3dr_v2 { +namespace iili { /// A live redirect target. expires_at rides along so a cached slug can go /// dark on time. @@ -30,6 +30,6 @@ class UrlStore { virtual absl::StatusOr> Lookup(const std::string& slug) = 0; }; -} // namespace r3dr_v2 +} // namespace iili -#endif // DOMAINS_R3DR_APIS_R3DR_V2_URL_STORE_H +#endif // DOMAINS_IILI_APIS_IILI_URL_STORE_H diff --git a/domains/r3dr/apis/r3dr_v2/wire_test.cc b/domains/iili/apis/iili/wire_test.cc similarity index 74% rename from domains/r3dr/apis/r3dr_v2/wire_test.cc rename to domains/iili/apis/iili/wire_test.cc index b7e200964..354b0058d 100644 --- a/domains/r3dr/apis/r3dr_v2/wire_test.cc +++ b/domains/iili/apis/iili/wire_test.cc @@ -9,16 +9,16 @@ #include #include "absl/time/time.h" -#include "domains/r3dr/apis/r3dr_v2/smithy_handler.h" -#include "domains/r3dr/apis/r3dr_v2/test_support.h" -#include "moonbase/r3dr/client.h" -#include "moonbase/r3dr/server.h" +#include "domains/iili/apis/iili/smithy_handler.h" +#include "domains/iili/apis/iili/test_support.h" +#include "moonbase/iili/client.h" +#include "moonbase/iili/server.h" #include "smithy/http/loopback.h" #include "smithy/http/transport.h" // The bytes on the wire, through the generated server: the v2 contract. -namespace r3dr_v2 { +namespace iili { namespace { using ::testing::HasSubstr; @@ -31,10 +31,10 @@ class WireTest : public testing::Test { WireTest() : store_(std::make_shared()), shortener_(MakeShortener(store_, [] { return kNow; })) { - server_ = std::make_unique( + server_ = std::make_unique( std::make_shared(shortener_)); loopback_ = std::make_shared(); - const auto started = loopback_->Start(server_->Handler()); + const auto started = loopback_->Start(WithHeadAsGet(server_->Handler())); EXPECT_TRUE(started.ok()); } @@ -54,12 +54,12 @@ class WireTest : public testing::Test { std::shared_ptr store_; std::shared_ptr shortener_; - std::unique_ptr server_; + std::unique_ptr server_; std::shared_ptr loopback_; }; TEST_F(WireTest, ShortenReturns201WithTheSlugAloneInTheBody) { - const auto response = Send("POST", "/r3dr/v2/shorten", + const auto response = Send("POST", "/iili/v1/shorten", R"({"longUrl":"https://www.example.com","expiresAt":1755003600000})"); EXPECT_EQ(response.status, 201); // One key, nothing else; the spelling is the contract. @@ -68,7 +68,7 @@ TEST_F(WireTest, ShortenReturns201WithTheSlugAloneInTheBody) { TEST_F(WireTest, AnOmittedExpiryIsA400) { const auto response = - Send("POST", "/r3dr/v2/shorten", R"({"longUrl":"https://www.example.com"})"); + Send("POST", "/iili/v1/shorten", R"({"longUrl":"https://www.example.com"})"); EXPECT_EQ(response.status, 400); EXPECT_THAT(response.body, HasSubstr("expiresAt")); } @@ -77,7 +77,7 @@ TEST_F(WireTest, AnOmittedExpiryIsA400) { TEST_F(WireTest, TheGeneratedServerEnforcesTheLongUrlTraits) { // Whole body once: the fieldList shape is the contract. const auto tooShort = - Send("POST", "/r3dr/v2/shorten", R"({"longUrl":"http://g.c","expiresAt":1755003600000})"); + Send("POST", "/iili/v1/shorten", R"({"longUrl":"http://g.c","expiresAt":1755003600000})"); EXPECT_EQ(tooShort.status, 400); EXPECT_EQ(tooShort.body, "{\"fieldList\":[{\"message\":\"Value with length 10 at '/longUrl' failed to satisfy" @@ -86,17 +86,17 @@ TEST_F(WireTest, TheGeneratedServerEnforcesTheLongUrlTraits) { " 10 at '/longUrl' failed to satisfy constraint: Member must have length between 11" " and 1000, inclusive\"}"); - const auto noProtocol = Send("POST", "/r3dr/v2/shorten", + const auto noProtocol = Send("POST", "/iili/v1/shorten", R"({"longUrl":"www.example.com/path","expiresAt":1755003600000})"); EXPECT_EQ(noProtocol.status, 400); EXPECT_THAT(noProtocol.body, HasSubstr("/longUrl")); - const auto tooLong = Send("POST", "/r3dr/v2/shorten", + const auto tooLong = Send("POST", "/iili/v1/shorten", std::string(R"({"longUrl":"https://example.com/)") + std::string(1000, 'a') + R"(","expiresAt":1755003600000})"); EXPECT_EQ(tooLong.status, 400); - const auto missing = Send("POST", "/r3dr/v2/shorten", R"({})"); + const auto missing = Send("POST", "/iili/v1/shorten", R"({})"); EXPECT_EQ(missing.status, 400); } @@ -104,12 +104,12 @@ TEST_F(WireTest, TheGeneratedServerEnforcesTheLongUrlTraits) { // the reason the bound is 11) and exactly 1000. TEST_F(WireTest, TheLengthBoundsAcceptTheirOwnEdges) { const auto shortest = - Send("POST", "/r3dr/v2/shorten", R"({"longUrl":"http://g.co","expiresAt":1755003600000})"); + Send("POST", "/iili/v1/shorten", R"({"longUrl":"http://g.co","expiresAt":1755003600000})"); EXPECT_EQ(shortest.status, 201); const std::string longest = "https://example.com/" + std::string(980, 'a'); ASSERT_EQ(longest.size(), 1000u); - const auto atMax = Send("POST", "/r3dr/v2/shorten", + const auto atMax = Send("POST", "/iili/v1/shorten", R"({"longUrl":")" + longest + R"(","expiresAt":1755003600000})"); EXPECT_EQ(atMax.status, 201); } @@ -119,7 +119,7 @@ TEST_F(WireTest, TheLengthBoundsAcceptTheirOwnEdges) { // twin proving error messages do reach bodies when meant to. TEST_F(WireTest, AStoreFailureIsANonLeaking500NotA404) { store_->fail_lookups = true; - const auto response = Send("GET", "/r3dr/v2/r/DAA", ""); + const auto response = Send("GET", "/iili/v1/r/DAA", ""); EXPECT_GE(response.status, 500); EXPECT_THAT(response.body, ::testing::Not(HasSubstr("password"))); EXPECT_THAT(response.body, ::testing::Not(HasSubstr("shared_postgres"))); @@ -128,12 +128,12 @@ TEST_F(WireTest, AStoreFailureIsANonLeaking500NotA404) { // The clock rules cannot be traits; they answer as the modeled error, and // the message names the rule. TEST_F(WireTest, TheExpiryRulesAnswerAsTheModeledError) { - const auto past = Send("POST", "/r3dr/v2/shorten", + const auto past = Send("POST", "/iili/v1/shorten", R"({"longUrl":"https://www.example.com","expiresAt":1754000000000})"); EXPECT_EQ(past.status, 400); EXPECT_THAT(past.body, HasSubstr("expiresAt is in the past")); - const auto tooFar = Send("POST", "/r3dr/v2/shorten", + const auto tooFar = Send("POST", "/iili/v1/shorten", R"({"longUrl":"https://www.example.com","expiresAt":1758000000000})"); EXPECT_EQ(tooFar.status, 400); EXPECT_THAT(tooFar.body, HasSubstr("30 days")); @@ -143,21 +143,38 @@ TEST_F(WireTest, TheExpiryRulesAnswerAsTheModeledError) { TEST_F(WireTest, RedirectIsA302WithLocationAndTheConformancePinnedBody) { store_->targets["DAA"] = Target{"https://www.example.com/target", kNow + absl::Hours(1)}; - const auto response = Send("GET", "/r3dr/v2/r/DAA", ""); + const auto response = Send("GET", "/iili/v1/r/DAA", ""); EXPECT_EQ(response.status, 302); EXPECT_EQ(response.headers.Get("Location").value_or(""), "https://www.example.com/target"); EXPECT_EQ(response.body, "{}"); } +// #1433: unfurlers and link checkers lead with HEAD, which the router would +// 405 on its own. +TEST_F(WireTest, HeadGetsTheSameRedirectAsGetWithNoBody) { + store_->targets["DAA"] = Target{"https://www.example.com/target", kNow + absl::Hours(1)}; + + const auto response = Send("HEAD", "/iili/v1/r/DAA", ""); + EXPECT_EQ(response.status, 302); + EXPECT_EQ(response.headers.Get("Location").value_or(""), "https://www.example.com/target"); + EXPECT_EQ(response.body, ""); +} + +TEST_F(WireTest, HeadOnAnUnknownSlugIsTheSame404StatusAsGet) { + const auto response = Send("HEAD", "/iili/v1/r/zzz", ""); + EXPECT_EQ(response.status, 404); + EXPECT_EQ(response.body, ""); +} + // Unknown and expired collapse into one exact 404 body: the reader can't // probe which it was. TEST_F(WireTest, UnknownAndExpiredSlugsShareTheOneModeledJson404) { - const auto unknown = Send("GET", "/r3dr/v2/r/zzz", ""); + const auto unknown = Send("GET", "/iili/v1/r/zzz", ""); EXPECT_EQ(unknown.status, 404); EXPECT_EQ(unknown.body, R"({"message":"no such link"})"); store_->targets["DAA"] = Target{"https://www.example.com", kNow - absl::Seconds(1)}; - const auto expired = Send("GET", "/r3dr/v2/r/DAA", ""); + const auto expired = Send("GET", "/iili/v1/r/DAA", ""); EXPECT_EQ(expired.status, 404); EXPECT_EQ(expired.body, unknown.body); } @@ -167,17 +184,17 @@ TEST_F(WireTest, UnknownAndExpiredSlugsShareTheOneModeledJson404) { TEST_F(WireTest, TheGeneratedClientRoundTripsBothOperations) { smithy::ClientConfig config; config.http_client = loopback_; - auto client = moonbase::r3dr::R3drV2Client::Create(std::move(config)); + auto client = moonbase::iili::IiliClient::Create(std::move(config)); ASSERT_TRUE(client.ok()) << client.error().message(); - moonbase::r3dr::ShortenInput shorten; + moonbase::iili::ShortenInput shorten; shorten.longUrl = "https://www.example.com/page"; shorten.expiresAt = 1755003600000; auto minted = client->Shorten(shorten); ASSERT_TRUE(minted.ok()) << minted.error().message(); EXPECT_EQ(minted->slug, "AQA"); - moonbase::r3dr::RedirectInput redirect; + moonbase::iili::RedirectInput redirect; redirect.slug = "AQA"; auto followed = client->Redirect(redirect); ASSERT_TRUE(followed.ok()) << followed.error().message(); @@ -187,25 +204,25 @@ TEST_F(WireTest, TheGeneratedClientRoundTripsBothOperations) { TEST_F(WireTest, TheClientReadsTheTypedErrors) { smithy::ClientConfig config; config.http_client = loopback_; - auto client = moonbase::r3dr::R3drV2Client::Create(std::move(config)); + auto client = moonbase::iili::IiliClient::Create(std::move(config)); ASSERT_TRUE(client.ok()); - moonbase::r3dr::RedirectInput unknown; + moonbase::iili::RedirectInput unknown; unknown.slug = "zzz"; auto missed = client->Redirect(unknown); ASSERT_FALSE(missed.ok()); EXPECT_EQ(missed.error().code(), "NotFoundError"); - moonbase::r3dr::ShortenInput past; + moonbase::iili::ShortenInput past; past.longUrl = "https://www.example.com"; past.expiresAt = 1754000000000; auto refused = client->Shorten(past); ASSERT_FALSE(refused.ok()); EXPECT_EQ(refused.error().code(), "InvalidRequestError"); - ASSERT_NE(refused.error().detail(), nullptr); - EXPECT_THAT(refused.error().detail()->message, + ASSERT_NE(refused.error().detail(), nullptr); + EXPECT_THAT(refused.error().detail()->message, HasSubstr("in the past")); } } // namespace -} // namespace r3dr_v2 +} // namespace iili diff --git a/domains/r3dr/apps/iili_web/.gitignore b/domains/iili/apps/iili_web/.gitignore similarity index 100% rename from domains/r3dr/apps/iili_web/.gitignore rename to domains/iili/apps/iili_web/.gitignore diff --git a/domains/r3dr/apps/iili_web/BUILD.bazel b/domains/iili/apps/iili_web/BUILD.bazel similarity index 100% rename from domains/r3dr/apps/iili_web/BUILD.bazel rename to domains/iili/apps/iili_web/BUILD.bazel diff --git a/domains/r3dr/apps/iili_web/README.md b/domains/iili/apps/iili_web/README.md similarity index 79% rename from domains/r3dr/apps/iili_web/README.md rename to domains/iili/apps/iili_web/README.md index 8168f5f9e..2732ca0bc 100644 --- a/domains/r3dr/apps/iili_web/README.md +++ b/domains/iili/apps/iili_web/README.md @@ -1,9 +1,9 @@ # iili -React + TypeScript frontend for the [r3dr_v2](../../apis/r3dr_v2) URL +React + TypeScript frontend for the [iili](../../apis/iili) URL shortener. Built on the 1d4_web stack (Vite, Tailwind, Vitest) and deployed to **Cloudflare Workers** at [iili.uk](https://iili.uk) — the shortener's -SPA. A second frontend for the same API lives at muchq.com/r3dr (the +SPA. A second frontend for the same API lives at muchq.com/iili (the muchq.github.io repo); both mint `i.iili.uk/r/{slug}` links. ## What it does @@ -15,7 +15,7 @@ muchq.github.io repo); both mint `i.iili.uk/r/{slug}` links. nothing is tracked server-side. - **Redirect** — short links resolve on `i.iili.uk` (A-record to the consolidated host). Caddy rewrites `GET|HEAD /r/{slug}` onto - `r3dr_v2`'s `/r3dr/v2/r/{slug}` so the API's per-client rate limit still + `iili`'s `/iili/v1/r/{slug}` so the API's per-client rate limit still keys on the real client. `HEAD` end-to-end 302 is #1433. This Worker is SPA-only. (`/r/`, not bare `/{slug}`: asset paths like `/assets` are themselves slug-shaped.) @@ -57,13 +57,10 @@ npx wrangler deploy --config dist/iili_web/wrangler.json Cloudflare CI is configured in the Workers dashboard: - **Build command:** `npm ci && npm run build` - **Deploy command:** `npx wrangler deploy --config dist/iili_web/wrangler.json` -- **Root directory:** `/domains/r3dr/apps/iili_web` +- **Root directory:** `/domains/iili/apps/iili_web` `iili.uk` is the SPA; `i.iili.uk` is the redirect edge on the consolidated -host. The r3dr.net Caddy frontage (and its short links) died with that -hosting move; the Go `r3dr` container remains in compose until #1359 -chunk 3 removes the service itself. No `iili.uk/r/{slug}` links were -minted in the #1430 Worker window — minting moved straight to `i.iili.uk`. +host. To smoke-test before custom domains attach: the `workers.dev` preview exercises the static UI, but can't mint — API CORS echoes only `iili.uk`, @@ -73,7 +70,7 @@ minted links hit the redirect host. ## Dependencies -- The r3dr_v2 API deployed at `api.muchq.com`. -- Redirect host `i.iili.uk` on the consolidated Caddy (rewrite → r3dr_v2). +- The iili API deployed at `api.muchq.com`. +- Redirect host `i.iili.uk` on the consolidated Caddy (rewrite → iili). - API CORS must allow origin `https://iili.uk` (and `http://localhost:5173` for `npm run dev`) — both set in `deploy/consolidated/Caddyfile`. diff --git a/domains/r3dr/apps/iili_web/index.html b/domains/iili/apps/iili_web/index.html similarity index 100% rename from domains/r3dr/apps/iili_web/index.html rename to domains/iili/apps/iili_web/index.html diff --git a/domains/r3dr/apps/iili_web/package-lock.json b/domains/iili/apps/iili_web/package-lock.json similarity index 99% rename from domains/r3dr/apps/iili_web/package-lock.json rename to domains/iili/apps/iili_web/package-lock.json index 226603ed4..d2ace6e93 100644 --- a/domains/r3dr/apps/iili_web/package-lock.json +++ b/domains/iili/apps/iili_web/package-lock.json @@ -1,11 +1,11 @@ { - "name": "r3dr-web", + "name": "iili-web", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "r3dr-web", + "name": "iili-web", "version": "0.0.0", "dependencies": { "react": "^19.0.0", diff --git a/domains/r3dr/apps/iili_web/package.json b/domains/iili/apps/iili_web/package.json similarity index 100% rename from domains/r3dr/apps/iili_web/package.json rename to domains/iili/apps/iili_web/package.json diff --git a/domains/r3dr/apps/iili_web/src/App.tsx b/domains/iili/apps/iili_web/src/App.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/App.tsx rename to domains/iili/apps/iili_web/src/App.tsx diff --git a/domains/r3dr/apps/iili_web/src/__tests__/App.test.tsx b/domains/iili/apps/iili_web/src/__tests__/App.test.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/__tests__/App.test.tsx rename to domains/iili/apps/iili_web/src/__tests__/App.test.tsx diff --git a/domains/r3dr/apps/iili_web/src/__tests__/CopyButton.test.tsx b/domains/iili/apps/iili_web/src/__tests__/CopyButton.test.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/__tests__/CopyButton.test.tsx rename to domains/iili/apps/iili_web/src/__tests__/CopyButton.test.tsx diff --git a/domains/r3dr/apps/iili_web/src/__tests__/ShortenCard.test.tsx b/domains/iili/apps/iili_web/src/__tests__/ShortenCard.test.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/__tests__/ShortenCard.test.tsx rename to domains/iili/apps/iili_web/src/__tests__/ShortenCard.test.tsx diff --git a/domains/r3dr/apps/iili_web/src/__tests__/api.test.ts b/domains/iili/apps/iili_web/src/__tests__/api.test.ts similarity index 98% rename from domains/r3dr/apps/iili_web/src/__tests__/api.test.ts rename to domains/iili/apps/iili_web/src/__tests__/api.test.ts index b893a30a4..cb0dd1d6f 100644 --- a/domains/r3dr/apps/iili_web/src/__tests__/api.test.ts +++ b/domains/iili/apps/iili_web/src/__tests__/api.test.ts @@ -23,7 +23,7 @@ describe('shorten', () => { expect(result).toEqual({ slug: 'AQA' }); const [url, init] = fetchMock.mock.calls[0]; - expect(url).toBe('https://api.muchq.com/r3dr/v2/shorten'); + expect(url).toBe('https://api.muchq.com/iili/v1/shorten'); expect(init.method).toBe('POST'); // Without this header the browser sends text/plain and drops out of the // preflighted class the Caddy CORS allow-list serves. diff --git a/domains/r3dr/apps/iili_web/src/__tests__/expiry.test.ts b/domains/iili/apps/iili_web/src/__tests__/expiry.test.ts similarity index 100% rename from domains/r3dr/apps/iili_web/src/__tests__/expiry.test.ts rename to domains/iili/apps/iili_web/src/__tests__/expiry.test.ts diff --git a/domains/r3dr/apps/iili_web/src/__tests__/recent.test.ts b/domains/iili/apps/iili_web/src/__tests__/recent.test.ts similarity index 100% rename from domains/r3dr/apps/iili_web/src/__tests__/recent.test.ts rename to domains/iili/apps/iili_web/src/__tests__/recent.test.ts diff --git a/domains/r3dr/apps/iili_web/src/__tests__/worker.test.ts b/domains/iili/apps/iili_web/src/__tests__/worker.test.ts similarity index 100% rename from domains/r3dr/apps/iili_web/src/__tests__/worker.test.ts rename to domains/iili/apps/iili_web/src/__tests__/worker.test.ts diff --git a/domains/r3dr/apps/iili_web/src/api.ts b/domains/iili/apps/iili_web/src/api.ts similarity index 92% rename from domains/r3dr/apps/iili_web/src/api.ts rename to domains/iili/apps/iili_web/src/api.ts index a030d31ae..dfcdf5164 100644 --- a/domains/r3dr/apps/iili_web/src/api.ts +++ b/domains/iili/apps/iili_web/src/api.ts @@ -1,9 +1,9 @@ -// The v2 API (domains/r3dr/apis/r3dr_v2), served at api.muchq.com behind +// The iili API (domains/iili/apis/iili), served at api.muchq.com behind // CORS for this origin. VITE_API_BASE overrides for local experiments. const API_BASE: string = (import.meta.env.VITE_API_BASE as string | undefined) ?? 'https://api.muchq.com'; -// Short links resolve on i.iili.uk (Caddy → r3dr_v2). The SPA is on +// Short links resolve on i.iili.uk (Caddy → iili). The SPA is on // Cloudflare at iili.uk; /r/{slug} is not a Worker path. VITE_SHORT_LINK_BASE // overrides for local / preview experiments. const SHORT_LINK_BASE: string = @@ -48,7 +48,7 @@ export const SHORTEN_TIMEOUT_MS = 10_000; export async function shorten(longUrl: string, expiresAt: number): Promise<{ slug: string }> { let res: Response; try { - res = await fetch(`${API_BASE}/r3dr/v2/shorten`, { + res = await fetch(`${API_BASE}/iili/v1/shorten`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ longUrl, expiresAt }), diff --git a/domains/r3dr/apps/iili_web/src/components/CopyButton.tsx b/domains/iili/apps/iili_web/src/components/CopyButton.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/components/CopyButton.tsx rename to domains/iili/apps/iili_web/src/components/CopyButton.tsx diff --git a/domains/r3dr/apps/iili_web/src/components/RecentLinks.tsx b/domains/iili/apps/iili_web/src/components/RecentLinks.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/components/RecentLinks.tsx rename to domains/iili/apps/iili_web/src/components/RecentLinks.tsx diff --git a/domains/r3dr/apps/iili_web/src/components/ShortenCard.tsx b/domains/iili/apps/iili_web/src/components/ShortenCard.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/components/ShortenCard.tsx rename to domains/iili/apps/iili_web/src/components/ShortenCard.tsx diff --git a/domains/r3dr/apps/iili_web/src/expiry.ts b/domains/iili/apps/iili_web/src/expiry.ts similarity index 100% rename from domains/r3dr/apps/iili_web/src/expiry.ts rename to domains/iili/apps/iili_web/src/expiry.ts diff --git a/domains/r3dr/apps/iili_web/src/index.css b/domains/iili/apps/iili_web/src/index.css similarity index 100% rename from domains/r3dr/apps/iili_web/src/index.css rename to domains/iili/apps/iili_web/src/index.css diff --git a/domains/r3dr/apps/iili_web/src/main.tsx b/domains/iili/apps/iili_web/src/main.tsx similarity index 100% rename from domains/r3dr/apps/iili_web/src/main.tsx rename to domains/iili/apps/iili_web/src/main.tsx diff --git a/domains/r3dr/apps/iili_web/src/recent.ts b/domains/iili/apps/iili_web/src/recent.ts similarity index 100% rename from domains/r3dr/apps/iili_web/src/recent.ts rename to domains/iili/apps/iili_web/src/recent.ts diff --git a/domains/r3dr/apps/iili_web/src/setupTests.ts b/domains/iili/apps/iili_web/src/setupTests.ts similarity index 100% rename from domains/r3dr/apps/iili_web/src/setupTests.ts rename to domains/iili/apps/iili_web/src/setupTests.ts diff --git a/domains/r3dr/apps/iili_web/src/vite-env.d.ts b/domains/iili/apps/iili_web/src/vite-env.d.ts similarity index 100% rename from domains/r3dr/apps/iili_web/src/vite-env.d.ts rename to domains/iili/apps/iili_web/src/vite-env.d.ts diff --git a/domains/r3dr/apps/iili_web/tsconfig.json b/domains/iili/apps/iili_web/tsconfig.json similarity index 100% rename from domains/r3dr/apps/iili_web/tsconfig.json rename to domains/iili/apps/iili_web/tsconfig.json diff --git a/domains/r3dr/apps/iili_web/vite.config.ts b/domains/iili/apps/iili_web/vite.config.ts similarity index 100% rename from domains/r3dr/apps/iili_web/vite.config.ts rename to domains/iili/apps/iili_web/vite.config.ts diff --git a/domains/r3dr/apps/iili_web/worker/index.d.ts b/domains/iili/apps/iili_web/worker/index.d.ts similarity index 100% rename from domains/r3dr/apps/iili_web/worker/index.d.ts rename to domains/iili/apps/iili_web/worker/index.d.ts diff --git a/domains/r3dr/apps/iili_web/worker/index.js b/domains/iili/apps/iili_web/worker/index.js similarity index 86% rename from domains/r3dr/apps/iili_web/worker/index.js rename to domains/iili/apps/iili_web/worker/index.js index 78d1f23ad..54c174c9b 100644 --- a/domains/r3dr/apps/iili_web/worker/index.js +++ b/domains/iili/apps/iili_web/worker/index.js @@ -1,6 +1,6 @@ /** * Cloudflare Worker — serves the SPA from Workers Assets. Short-link - * redirects live on i.iili.uk (Caddy → r3dr_v2), not here: keeping them + * redirects live on i.iili.uk (Caddy → iili), not here: keeping them * off this Worker means the API's per-client rate limit keys on the real * client via the consolidated host, and a compat-date bump cannot hand * /r/* to the SPA shell. diff --git a/domains/r3dr/apps/iili_web/wrangler.toml b/domains/iili/apps/iili_web/wrangler.toml similarity index 96% rename from domains/r3dr/apps/iili_web/wrangler.toml rename to domains/iili/apps/iili_web/wrangler.toml index ec535427c..65beb1e32 100644 --- a/domains/r3dr/apps/iili_web/wrangler.toml +++ b/domains/iili/apps/iili_web/wrangler.toml @@ -5,7 +5,7 @@ main = "worker/index.js" # assets_navigation_prefers_asset_serving cannot swallow /r/*. compatibility_date = "2026-08-21" -# Domain: iili.uk (SPA). Redirects: i.iili.uk → consolidated Caddy → r3dr_v2. +# Domain: iili.uk (SPA). Redirects: i.iili.uk → consolidated Caddy → iili. [assets] directory = "dist/client" diff --git a/domains/platform/apis/prom_proxy/registry.go b/domains/platform/apis/prom_proxy/registry.go index 6486ad1cf..3b176d196 100644 --- a/domains/platform/apis/prom_proxy/registry.go +++ b/domains/platform/apis/prom_proxy/registry.go @@ -263,7 +263,7 @@ type serviceEntry struct { // aura::Cache emitters share the standard cache family (#1209), selected on // both labels: which service, and which cache within it. Parameterized — -// the prom_proxy half of #1209 — now that r3dr_v2's url_cache is the second +// the prom_proxy half of #1209 — now that iili's url_cache is the second // emitter (#1359). func cacheHitPercent(service, cache string) string { hit := fmt.Sprintf(`rate(cache_hits_total{service_name=%q,cache=%q}[5m])`, service, cache) @@ -303,7 +303,7 @@ const ( ) // Catalog order doubles as the UI's tab order. -var serviceOrder = []string{"golf_hub", "mcpserver", "microgpt-serve", "mithril", "one_d4", "one_d4_v2", "portrait", "posterize", "r3dr_v2"} +var serviceOrder = []string{"golf_hub", "mcpserver", "microgpt-serve", "mithril", "one_d4", "one_d4_v2", "portrait", "posterize", "iili"} var serviceRegistry = map[string]serviceEntry{ "golf_hub": { @@ -482,16 +482,16 @@ var serviceRegistry = map[string]serviceEntry{ probesTile("one_d4_v2"), }, }, - // r3dr v2 (#1359): standard instruments, Probes, and the URL cache. - "r3dr_v2": { + // iili (#1359): standard instruments, Probes, and the URL cache. + "iili": { CustomScalars: []customScalarDef{ - probesTile("r3dr_v2"), - scalar("URL cache", "hit_rate_percent", "%", cacheHitPercent("r3dr_v2", "url_cache")), - counter("URL cache", "operations", "", cacheOps("r3dr_v2", "url_cache")), + probesTile("iili"), + scalar("URL cache", "hit_rate_percent", "%", cacheHitPercent("iili", "url_cache")), + counter("URL cache", "operations", "", cacheOps("iili", "url_cache")), }, CustomTimeseries: map[string]customTimeseriesDef{ - "cache_hit_rate": tsFixed(cacheHitPercent("r3dr_v2", "url_cache")), - "cache_operations": tsCounter(cacheOps("r3dr_v2", "url_cache")), + "cache_hit_rate": tsFixed(cacheHitPercent("iili", "url_cache")), + "cache_operations": tsCounter(cacheOps("iili", "url_cache")), }, }, // Wordchains: server_pal's standard instruments plus the standard diff --git a/domains/platform/apis/prom_proxy/registry_test.go b/domains/platform/apis/prom_proxy/registry_test.go index 575faa842..76716a33c 100644 --- a/domains/platform/apis/prom_proxy/registry_test.go +++ b/domains/platform/apis/prom_proxy/registry_test.go @@ -182,14 +182,14 @@ func allQueriesFor(entry serviceEntry) []string { // strings are the contract with Prometheus. func TestRegistry_CacheHelpersEmitTheExactQueries(t *testing.T) { assert.Equal(t, - `rate(cache_hits_total{service_name="r3dr_v2",cache="url_cache"}[5m])`+ - `/(rate(cache_hits_total{service_name="r3dr_v2",cache="url_cache"}[5m])`+ - `+rate(cache_misses_total{service_name="r3dr_v2",cache="url_cache"}[5m]))*100`, - cacheHitPercent("r3dr_v2", "url_cache")) + `rate(cache_hits_total{service_name="iili",cache="url_cache"}[5m])`+ + `/(rate(cache_hits_total{service_name="iili",cache="url_cache"}[5m])`+ + `+rate(cache_misses_total{service_name="iili",cache="url_cache"}[5m]))*100`, + cacheHitPercent("iili", "url_cache")) assert.Equal(t, `{__name__=~"cache_hits_total|cache_misses_total",`+ - `service_name="r3dr_v2",cache="url_cache"}`, - cacheOps("r3dr_v2", "url_cache")) + `service_name="iili",cache="url_cache"}`, + cacheOps("iili", "url_cache")) } // Cache queries follow the emitter: aura::Cache emits the standard cache @@ -203,7 +203,7 @@ func TestRegistry_CacheQueriesUseTheStandardFamily(t *testing.T) { cache string }{ {"portrait", "trace"}, - {"r3dr_v2", "url_cache"}, + {"iili", "url_cache"}, } { entry := serviceRegistry[emitter.service] diff --git a/domains/platform/apis/prom_proxy/service_handlers_test.go b/domains/platform/apis/prom_proxy/service_handlers_test.go index 8671ebac6..2d67661e4 100644 --- a/domains/platform/apis/prom_proxy/service_handlers_test.go +++ b/domains/platform/apis/prom_proxy/service_handlers_test.go @@ -61,7 +61,7 @@ func TestMetricsHandler_GetServiceCatalog(t *testing.T) { {"one_d4_v2", true}, {"portrait", true}, {"posterize", true}, - {"r3dr_v2", true}, + {"iili", true}, } require.Len(t, catalog.Services, len(wantCustom)) for i, want := range wantCustom { diff --git a/domains/r3dr/README.md b/domains/r3dr/README.md deleted file mode 100644 index c1f21843a..000000000 --- a/domains/r3dr/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# r3dr Domain - -URL shortening services and related applications. - -## APIs - -- [**r3dr_v2 API**](apis/r3dr_v2): The C++ shortener on smithy-cpp (#1359), - serving `/r3dr/v2/*` behind `api.muchq.com`. - -## Apps - -- [**iili**](apps/iili_web): The standalone frontend (#1359) — a - Cloudflare Worker SPA at `iili.uk`. Short links mint as - `i.iili.uk/r/{slug}` (Caddy on the consolidated host → r3dr_v2). A - second frontend lives at muchq.com/r3dr (the muchq.github.io repo). diff --git a/domains/r3dr/apis/r3dr_v2/model/bindings/simplerestjson.smithy b/domains/r3dr/apis/r3dr_v2/model/bindings/simplerestjson.smithy deleted file mode 100644 index d86b47bb2..000000000 --- a/domains/r3dr/apis/r3dr_v2/model/bindings/simplerestjson.smithy +++ /dev/null @@ -1,10 +0,0 @@ -// Protocol binding overlay: binds the protocol-agnostic R3drV2 service to -// alloy#simpleRestJson (plain REST/JSON). Passed alongside model/r3dr_v2.smithy -// to the generation rules. -$version: "2.0" - -namespace moonbase.r3dr - -use alloy#simpleRestJson - -apply R3drV2 @simpleRestJson diff --git a/domains/r3dr/apis/r3dr_v2/smithy_handler.h b/domains/r3dr/apis/r3dr_v2/smithy_handler.h deleted file mode 100644 index 736009a9c..000000000 --- a/domains/r3dr/apis/r3dr_v2/smithy_handler.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef DOMAINS_R3DR_APIS_R3DR_V2_SMITHY_HANDLER_H -#define DOMAINS_R3DR_APIS_R3DR_V2_SMITHY_HANDLER_H - -#include -#include - -#include "absl/status/status.h" -#include "domains/r3dr/apis/r3dr_v2/shortener.h" -#include "moonbase/r3dr/server.h" -#include "smithy/core/error.h" -#include "smithy/core/outcome.h" -#include "smithy/server/router.h" - -namespace r3dr_v2 { - -/// kInvalidArgument -> InvalidRequestError (400); everything else (the -/// store) -> unmodeled 500 with a fixed body. -smithy::Error ToSmithyError(const absl::Status& status); - -/// Type-mapping shim over Shortener, whose pieces (aura::Cache, pg::Client) -/// are themselves thread-safe — the whole story for the transport's pool. -class SmithyShortenerHandler final : public moonbase::r3dr::R3drV2Handler { - public: - explicit SmithyShortenerHandler(std::shared_ptr shortener) - : shortener_(std::move(shortener)) {} - - smithy::Outcome Shorten( - const moonbase::r3dr::ShortenInput& input, - const smithy::server::RequestContext& context) override; - - /// A miss is the modeled 404; a store failure is a 500, not a 404. - smithy::Outcome Redirect( - const moonbase::r3dr::RedirectInput& input, - const smithy::server::RequestContext& context) override; - - private: - std::shared_ptr shortener_; -}; - -} // namespace r3dr_v2 - -#endif // DOMAINS_R3DR_APIS_R3DR_V2_SMITHY_HANDLER_H diff --git a/go.mod b/go.mod index 02e80326e..db8e8216c 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/lib/pq v1.12.3 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.69.0 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 4004a482c..9c1e6849c 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -31,8 +29,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= -github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -84,8 +80,6 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=