repo: scrub luan.chat (a personal domain) from the codebase (#316)
* repo: scrub luan.chat (a personal domain) from the codebase
luan.chat is a personal family instance, not product infrastructure, so
it shouldn't be baked into examples, tests, or shipped config. Replaced
throughout (CLAUDE.local.md, the operator runbook, deliberately keeps
its real references):
- Doc / test / placeholder examples → example.com (subdomain prefixes
preserved, e.g. test.luan.chat → test.example.com; test assertions
changed input + expected together so they stay valid).
- Functional refs → the product domain elevenmessenger.com:
- ios/LChat/LChat.entitlements associated-domains applinks:*.luan.chat
→ applinks:*.elevenmessenger.com (NB: functional — needs an app
rebuild + AASA served on that host + Apple validation before
universal links activate there).
- push.go VAPID Subscriber admin@luan.chat → admin@elevenmessenger.com.
- unfurl.go link-preview User-Agent +https://luan.chat → …elevenmessenger.com.
- test/knowledge.test.mjs: dropped luan.chat from the "bot must never
leak a real hostname" guard (run.garden / elevenmessenger.com / paulca
still guarded).
- Dead Miren deploy scripts: MIREN_CLUSTER default luanchat → mycluster.
go build/vet/test, npm test all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: drop luan.chat from the bot hostname-leak guard (merge follow-up)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
28 files changed,
+67
−67
.miren/agent.app.toml+1 −1.miren/app.toml+1 −1README.md+6 −6agent/README.md+3 −3agent/config.mjs+2 −2broker/broker_test.go+4 −4broker/db.go+2 −2broker/main.go+1 −1control_test.go+1 −1deploy/go-router.sh+1 −1deploy/miren.sh+2 −2deploy/miren/agent.sh+2 −2deploy/miren/home.sh+1 −1deploy/miren/push.sh+1 −1docs/connected-services.md+1 −1docs/instance-router.md+4 −4docs/onboarding-and-home-server.md+1 −1home/test/crypto.test.mjs+2 −2home/web/static/enroll.js+1 −1internal/bot/eleven_test.go+6 −6ios/LChat/LChat.entitlements+1 −1ios/LChatCore/Tests/LChatCoreTests/InviteLinkTests.swift+10 −10push.go+1 −1push/db.go+1 −1push/push_test.go+5 −5test/knowledge.test.mjs+1 −1unfurl.go+1 −1versions_test.go+4 −4
diff --git a/.miren/agent.app.toml b/.miren/agent.app.toml| index c36877a..28d27bf 100644 |
| --- a/.miren/agent.app.toml |
| +++ b/.miren/agent.app.toml |
| @@ -38,7 +38,7 @@ key = "LCHAT_ELEVEN_SESSION" |
| value = "/data/eleven-session.json" |
| # Set at deploy time via `-e` (see deploy/miren/agent.sh), NOT declared here: |
| -# LCHAT_BASE the instance to connect to, e.g. https://test-bots.luan.chat |
| +# LCHAT_BASE the instance to connect to, e.g. https://test-bots.example.com |
| # ANTHROPIC_API_KEY billing credential — lives only in this app's env |
| # LCHAT_INVITE one-time invite #fragment; used only on first boot |
| # LCHAT_ANNOUNCER_INVITE same, for the "Announcer" identity |
diff --git a/.miren/app.toml b/.miren/app.toml| index 563318a..58b2363 100644 |
| --- a/.miren/app.toml |
| +++ b/.miren/app.toml |
| @@ -41,4 +41,4 @@ value = "/data/lchat.db" |
| # 2. Use `-e` (plain), not `-s` (sensitive) — Miren stores sensitive vars but |
| # does not inject them into the container. |
| # It is NOT a decryption key (messages stay end-to-end encrypted); it only gates |
| -# the invite backend. Admin URL: https://group.luan.chat/admin#token=<token> |
| +# the invite backend. Admin URL: https://group.example.com/admin#token=<token> |
diff --git a/README.md b/README.md| index a2c0b72..ff1c508 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -235,11 +235,11 @@ TOKEN=$(openssl rand -base64 32 | tr -d '\n' | tr '+/' '-_' | tr -d '=') |
| # 3. Deploy (pass the token as a plain -e env var) and attach the domain: |
| miren deploy -e LCHAT_DB=/data/lchat.db -e "LCHAT_ADMIN_TOKEN=$TOKEN" |
| -miren route set group.luan.chat lchat |
| -# Your admin URL is then: https://group.luan.chat/admin#token=$TOKEN |
| +miren route set group.example.com lchat |
| +# Your admin URL is then: https://group.example.com/admin#token=$TOKEN |
| ``` |
| -Then point DNS for `group.luan.chat` at the cluster (a CNAME to the |
| +Then point DNS for `group.example.com` at the cluster (a CNAME to the |
| `*.miren.systems` hostname Miren prints, or an A record to the server IP). Once |
| DNS resolves, Miren provisions TLS automatically. |
| @@ -248,14 +248,14 @@ DNS resolves, Miren provisions TLS automatically. |
| keys, group name) all survive. **Do not `miren app delete` to update** — that |
| deletes the disk and wipes everything; use it only to intentionally reset. |
| -**More instances from one repo:** `./deploy/miren.sh <name> <name>.luan.chat` |
| +**More instances from one repo:** `./deploy/miren.sh <name> <name>.example.com` |
| gives each its own app + disk + token + route (re-run it to update an instance; |
| it preserves that instance's data too). |
| Your admin URL is then: |
| ``` |
| -https://group.luan.chat/admin#token=<ADMIN_TOKEN> |
| +https://group.example.com/admin#token=<ADMIN_TOKEN> |
| ``` |
| > WebSockets: the live feed uses a standard HTTP upgrade on `/ws`. Miren's |
| @@ -303,7 +303,7 @@ hands the server only ciphertext — exactly like the browser does. |
| ```sh |
| # text as an argument |
| -tools/lchat-post.mjs --host https://group.luan.chat \ |
| +tools/lchat-post.mjs --host https://group.example.com \ |
| --token "$TOKEN" --priv "$PRIV" --thread "$TID" "deploy finished ✅" |
| # or pipe it in, with a custom screen name (config via env) |
diff --git a/agent/README.md b/agent/README.md| index 0ee31b9..713bab0 100644 |
| --- a/agent/README.md |
| +++ b/agent/README.md |
| @@ -26,7 +26,7 @@ console, its `#fragment` passed once as the matching `*_INVITE` env var. |
| | | | |
| |---|---| |
| | `ANTHROPIC_API_KEY` | The only recurring secret + cost. Every DM and every group @mention is one Claude API call billed to your account (bots in the "name" listen mode also spend a fraction-of-a-cent Haiku call per bare-name mention to decide whether to answer — `LCHAT_CLASSIFIER_MODEL` overrides). | |
| -| `LCHAT_BASE` | Your instance URL, e.g. `https://test.luan.chat`. | |
| +| `LCHAT_BASE` | Your instance URL, e.g. `https://test.example.com`. | |
| | `LCHAT_INVITE` | **One-time.** The `#fragment` of a `/join/…` link minted in the admin console (tick "This is an agent"). Spent after provisioning. | |
| Optional: `LCHAT_AGENT_NAME` (default `Claude`), `LCHAT_MODEL` (default |
| @@ -42,12 +42,12 @@ cd agent |
| npm install |
| # 1. Provision once (creates session.json — the bot's identity; back it up) |
| -LCHAT_BASE=https://test.luan.chat LCHAT_INVITE='<#fragment from the invite link>' \ |
| +LCHAT_BASE=https://test.example.com LCHAT_INVITE='<#fragment from the invite link>' \ |
| npm run provision |
| # 2. Run |
| export ANTHROPIC_API_KEY=sk-ant-... |
| -LCHAT_BASE=https://test.luan.chat npm start |
| +LCHAT_BASE=https://test.example.com npm start |
| ``` |
| Then, in the app, add the agent to a group or start a DM with it. It replies to |
diff --git a/agent/config.mjs b/agent/config.mjs| index 5a4c2aa..61ec2cf 100644 |
| --- a/agent/config.mjs |
| +++ b/agent/config.mjs |
| @@ -6,7 +6,7 @@ |
| import { readFileSync, writeFileSync, existsSync } from "node:fs"; |
| import { fileURLToPath } from "node:url"; |
| -// The instance base URL, e.g. https://test.luan.chat. WS base is derived |
| +// The instance base URL, e.g. https://test.example.com. WS base is derived |
| // (https -> wss, http -> ws). |
| export const BASE = (process.env.LCHAT_BASE || "").replace(/\/$/, ""); |
| export const WS_BASE = BASE.replace(/^http/, "ws"); |
| @@ -33,7 +33,7 @@ export const SESSION_PATH = |
| export function requireBase() { |
| if (!BASE) { |
| - console.error("Set LCHAT_BASE to your instance URL, e.g. https://test.luan.chat"); |
| + console.error("Set LCHAT_BASE to your instance URL, e.g. https://test.example.com"); |
| process.exit(1); |
| } |
| } |
diff --git a/broker/broker_test.go b/broker/broker_test.go| index eca8009..5384e3f 100644 |
| --- a/broker/broker_test.go |
| +++ b/broker/broker_test.go |
| @@ -61,7 +61,7 @@ func fakeDNSimple(t *testing.T) *httptest.Server { |
| func TestExchange(t *testing.T) { |
| dns := fakeDNSimple(t) |
| ts := newTestBroker(t, dns.URL) |
| - _, tok, err := testDB.mintToken("https://test.luan.chat") |
| + _, tok, err := testDB.mintToken("https://test.example.com") |
| if err != nil { |
| t.Fatalf("mint: %v", err) |
| } |
| @@ -115,12 +115,12 @@ func TestConfig(t *testing.T) { |
| // and never to an unregistered one (no open redirect). |
| func TestCallbackRelay(t *testing.T) { |
| ts := newTestBroker(t, "http://unused") |
| - if _, _, err := testDB.mintToken("https://test.luan.chat"); err != nil { |
| + if _, _, err := testDB.mintToken("https://test.example.com"); err != nil { |
| t.Fatalf("mint: %v", err) |
| } |
| noRedirect := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} |
| - good := base64.RawURLEncoding.EncodeToString([]byte("https://test.luan.chat")) + ".nonce123" |
| + good := base64.RawURLEncoding.EncodeToString([]byte("https://test.example.com")) + ".nonce123" |
| resp, err := noRedirect.Get(ts.URL + "/oauth/dnsimple/callback?code=XYZ&state=" + good) |
| if err != nil { |
| t.Fatalf("callback: %v", err) |
| @@ -130,7 +130,7 @@ func TestCallbackRelay(t *testing.T) { |
| t.Fatalf("callback: got %d want 302", resp.StatusCode) |
| } |
| loc := resp.Header.Get("Location") |
| - if !strings.HasPrefix(loc, "https://test.luan.chat/oauth/dnsimple/callback?") || !strings.Contains(loc, "code=XYZ") || !strings.Contains(loc, "state=") { |
| + if !strings.HasPrefix(loc, "https://test.example.com/oauth/dnsimple/callback?") || !strings.Contains(loc, "code=XYZ") || !strings.Contains(loc, "state=") { |
| t.Fatalf("relay Location = %q", loc) |
| } |
diff --git a/broker/db.go b/broker/db.go| index 9f78883..986e977 100644 |
| --- a/broker/db.go |
| +++ b/broker/db.go |
| @@ -12,7 +12,7 @@ import ( |
| ) |
| // DB holds the broker's only state: the per-instance bearer tokens the admin has |
| -// minted. Each token's label IS the instance's origin (e.g. "https://test.luan.chat") |
| +// minted. Each token's label IS the instance's origin (e.g. "https://test.example.com") |
| // — used both for logging and as the redirect-relay allowlist, so the relay only |
| // ever bounces a code back to an origin an admin has explicitly registered. It |
| // stores NO OAuth traffic — no codes, no access tokens ever touch disk. Tokens |
| @@ -45,7 +45,7 @@ func (db *DB) migrate() error { |
| _, err := db.sql.Exec(` |
| CREATE TABLE IF NOT EXISTS instance_tokens ( |
| id TEXT PRIMARY KEY, -- random public handle, used to revoke |
| - label TEXT NOT NULL, -- the instance ORIGIN (scheme+host), e.g. https://test.luan.chat |
| + label TEXT NOT NULL, -- the instance ORIGIN (scheme+host), e.g. https://test.example.com |
| token_hash TEXT NOT NULL UNIQUE, -- sha256(bearer token); the secret itself is never stored |
| created_at INTEGER NOT NULL, |
| last_used_at INTEGER |
diff --git a/broker/main.go b/broker/main.go| index 01179cb..746ba5e 100644 |
| --- a/broker/main.go |
| +++ b/broker/main.go |
| @@ -276,7 +276,7 @@ func (s *Server) admin(next http.HandlerFunc) http.HandlerFunc { |
| func (s *Server) mintHandler(w http.ResponseWriter, r *http.Request) { |
| var body struct { |
| - Label string `json:"label"` // the instance ORIGIN, e.g. https://test.luan.chat |
| + Label string `json:"label"` // the instance ORIGIN, e.g. https://test.example.com |
| } |
| if json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<10)).Decode(&body) != nil || body.Label == "" { |
| http.Error(w, "label (instance origin) required", http.StatusBadRequest) |
diff --git a/control_test.go b/control_test.go| index 503ac0b..8f5537b 100644 |
| --- a/control_test.go |
| +++ b/control_test.go |
| @@ -40,7 +40,7 @@ func TestReorderDoesNotPullFlagAsValue(t *testing.T) { |
| } |
| func TestValidHost(t *testing.T) { |
| - ok := []string{"chat.example.com", "rtr-test.luan.chat", "a.b", "x1.y2.example.com"} |
| + ok := []string{"chat.example.com", "rtr-test.example.com", "a.b", "x1.y2.example.com"} |
| bad := []string{"", "nodots", "-bad.example.com", "bad-.example.com", "has space.com", "Chat.Example.com", "a/b.com", "a..b"} |
| for _, h := range ok { |
| if !validHost(h) { |
diff --git a/deploy/go-router.sh b/deploy/go-router.sh| index de2ecd5..f03e8ef 100755 |
| --- a/deploy/go-router.sh |
| +++ b/deploy/go-router.sh |
| @@ -10,7 +10,7 @@ |
| # |
| # Usage: ./deploy/go-router.sh [host] |
| # ./deploy/go-router.sh # host defaults to team.elevenmessenger.com |
| -# ./deploy/go-router.sh test.luan.chat |
| +# ./deploy/go-router.sh test.example.com |
| # SKIP_BOT=1 ./deploy/go-router.sh # skip the bot-ensure step |
| # ANTHROPIC_API_KEY=sk-ant-... ./deploy/go-router.sh # AI-enabled instead |
| # |
diff --git a/deploy/miren.sh b/deploy/miren.sh| index 91070cf..cfb1060 100755 |
| --- a/deploy/miren.sh |
| +++ b/deploy/miren.sh |
| @@ -4,7 +4,7 @@ |
| # no per-instance config files. |
| # |
| # Usage: ./deploy/miren.sh <instance> <hostname> [admin-token] |
| -# ./deploy/miren.sh paja paja.luan.chat |
| +# ./deploy/miren.sh paja paja.example.com |
| # |
| # Miren's app.toml has no variable interpolation, so we template a throwaway copy |
| # in a temp dir (unique app name + unique disk name) and deploy from there. |
| @@ -13,7 +13,7 @@ set -euo pipefail |
| INSTANCE="${1:?usage: deploy/miren.sh <instance> <hostname> [admin-token]}" |
| HOST="${2:?usage: deploy/miren.sh <instance> <hostname> [admin-token]}" |
| TOKEN="${3:-$(openssl rand -base64 32 | tr -d '\n' | tr '+/' '-_' | tr -d '=')}" |
| -CLUSTER="${MIREN_CLUSTER:-luanchat}" |
| +CLUSTER="${MIREN_CLUSTER:-mycluster}" |
| REPO="$(cd "$(dirname "$0")/.." && pwd)" |
| TMP="$(mktemp -d)" |
diff --git a/deploy/miren/agent.sh b/deploy/miren/agent.sh| index a51eb44..c00812b 100755 |
| --- a/deploy/miren/agent.sh |
| +++ b/deploy/miren/agent.sh |
| @@ -19,8 +19,8 @@ |
| set -euo pipefail |
| INSTANCE="${1:?usage: deploy/miren/agent.sh <instance>}" |
| -: "${LCHAT_BASE:?set LCHAT_BASE (e.g. https://test-bots.luan.chat)}" |
| -CLUSTER="${MIREN_CLUSTER:-luanchat}" |
| +: "${LCHAT_BASE:?set LCHAT_BASE (e.g. https://test-bots.example.com)}" |
| +CLUSTER="${MIREN_CLUSTER:-mycluster}" |
| APP="${INSTANCE}-bot" |
| REPO="$(cd "$(dirname "$0")/../.." && pwd)" |
diff --git a/deploy/miren/home.sh b/deploy/miren/home.sh| index 787a514..749c9b6 100755 |
| --- a/deploy/miren/home.sh |
| +++ b/deploy/miren/home.sh |
| @@ -15,7 +15,7 @@ set -euo pipefail |
| INSTANCE="${1:?usage: deploy/miren/home.sh <instance> <hostname>}" |
| HOST="${2:?usage: deploy/miren/home.sh <instance> <hostname>}" |
| -CLUSTER="${MIREN_CLUSTER:-luanchat}" |
| +CLUSTER="${MIREN_CLUSTER:-mycluster}" |
| REPO="$(cd "$(dirname "$0")/../.." && pwd)" |
| TMP="$(mktemp -d)" |
diff --git a/deploy/miren/push.sh b/deploy/miren/push.sh| index cc44833..0958a15 100755 |
| --- a/deploy/miren/push.sh |
| +++ b/deploy/miren/push.sh |
| @@ -20,7 +20,7 @@ set -euo pipefail |
| INSTANCE="${1:?usage: deploy/miren/push.sh <instance> <hostname> [admin-token]}" |
| HOST="${2:?usage: deploy/miren/push.sh <instance> <hostname> [admin-token]}" |
| TOKEN="${3:-$(openssl rand -base64 32 | tr -d '\n' | tr '+/' '-_' | tr -d '=')}" |
| -CLUSTER="${MIREN_CLUSTER:-luanchat}" |
| +CLUSTER="${MIREN_CLUSTER:-mycluster}" |
| for v in LCHAT_APNS_KEY LCHAT_APNS_KEY_ID LCHAT_APNS_TEAM_ID LCHAT_APNS_TOPIC; do |
| [ -n "${!v:-}" ] || { echo "error: $v must be set (the .p8 signing config lives only on the relay)" >&2; exit 1; } |
diff --git a/docs/connected-services.md b/docs/connected-services.md| index ac3f96e..a09d585 100644 |
| --- a/docs/connected-services.md |
| +++ b/docs/connected-services.md |
| @@ -1,7 +1,7 @@ |
| # 🤖 Connecting third-party services (`/register`, and friends) |
| Status: **prototype shipping.** A connect broker is live at |
| -`connect.elevenmessenger.com` and `test-bots.luan.chat` is wired to it (branch |
| +`connect.elevenmessenger.com` and `test-bots.example.com` is wired to it (branch |
| `connect-oauth`). DNSimple is the worked example; the shape generalises to any |
| OAuth API. **The plan evolved during implementation** — DNSimple turned out to be |
| confidential-only, so the public/PKCE sections further down are **superseded** by |
diff --git a/docs/instance-router.md b/docs/instance-router.md| index 689ae54..1f2e1d0 100644 |
| --- a/docs/instance-router.md |
| +++ b/docs/instance-router.md |
| @@ -354,8 +354,8 @@ independent of every other's: |
| version (`lchat instances` shows it). |
| ``` |
| - /opt/lchat/live/test.luan.chat ─▶ versions/<canary>/lchat (one instance moved) |
| - /opt/lchat/live/family.luan.chat ─▶ versions/<stable>/lchat |
| + /opt/lchat/live/test.example.com ─▶ versions/<canary>/lchat (one instance moved) |
| + /opt/lchat/live/family.example.com ─▶ versions/<stable>/lchat |
| /opt/lchat/live/paulca… ─▶ versions/<stable>/lchat |
| ``` |
| @@ -365,10 +365,10 @@ independent of every other's: |
| ./deploy/build-linux.sh # build linux/amd64, stamped with the git sha |
| scp lchat-linux-amd64 root@box:/opt/lchat/incoming |
| ssh box lchat stage --file /opt/lchat/incoming # register it → prints <ver> |
| -ssh box lchat deploy test.luan.chat --version <ver> # canary ONE instance |
| +ssh box lchat deploy test.example.com --version <ver> # canary ONE instance |
| # …verify test… |
| ssh box lchat deploy --all --version <ver> # promote the rest |
| -ssh box lchat rollback test.luan.chat # or step back to its previous version |
| +ssh box lchat rollback test.example.com # or step back to its previous version |
| ``` |
| `stage` copies a binary into the store (reading its own build id via `lchat |
diff --git a/docs/onboarding-and-home-server.md b/docs/onboarding-and-home-server.md| index d080a0c..a7c624e 100644 |
| --- a/docs/onboarding-and-home-server.md |
| +++ b/docs/onboarding-and-home-server.md |
| @@ -224,7 +224,7 @@ Two mechanisms, deliberately ranked: |
| serves `/.well-known/apple-app-site-association`. Invite links on associated |
| domains open straight into the app — full URL, fragment included — and fall |
| back to the web welcome page when the app's absent. Limitation: the |
| - associated-domains entitlement is a static list (`*.luan.chat` + named |
| + associated-domains entitlement is a static list (`*.example.com` + named |
| custom domains); arbitrary custom domains can't join it. (WebAuthn Related |
| Origin Requests could stretch things later; parked.) |
| - **Custom scheme `elevenmessenger://` (explicit fallback).** One-line |
diff --git a/home/test/crypto.test.mjs b/home/test/crypto.test.mjs| index f8aec1a..31b165c 100644 |
| --- a/home/test/crypto.test.mjs |
| +++ b/home/test/crypto.test.mjs |
| @@ -65,7 +65,7 @@ await test("sealBlob → openBlob round-trips via a PRF-derived KEK", async () = |
| const prf = fakePRF(); |
| const kek = await kekFromPRF(prf); |
| const dek = await generateDEK(); |
| - const obj = { v: 1, servers: [{ url: "https://family.luan.chat", name: "Family" }] }; |
| + const obj = { v: 1, servers: [{ url: "https://family.example.com", name: "Family" }] }; |
| const sealed = await sealBlob(obj, dek, { "cred-1": await wrapDEK(dek, kek) }); |
| const { keys } = unpackBlob(sealed); |
| @@ -86,7 +86,7 @@ await test("envelope wraps the DEK per credential: each of 2 KEKs can open", asy |
| const prfA = fakePRF(); |
| const prfB = fakePRF(); |
| const dek = await generateDEK(); |
| - const obj = { v: 1, servers: [{ url: "https://test.luan.chat" }] }; |
| + const obj = { v: 1, servers: [{ url: "https://test.example.com" }] }; |
| const sealed = await sealBlob(obj, dek, { |
| "cred-a": await wrapDEK(dek, await kekFromPRF(prfA)), |
| "cred-b": await wrapDEK(dek, await kekFromPRF(prfB)), |
diff --git a/home/web/static/enroll.js b/home/web/static/enroll.js| index 0e7d3d5..c5ff81b 100644 |
| --- a/home/web/static/enroll.js |
| +++ b/home/web/static/enroll.js |
| @@ -1,4 +1,4 @@ |
| -// The enrolment page. An instance (e.g. family.luan.chat) navigates here (same |
| +// The enrolment page. An instance (e.g. family.example.com) navigates here (same |
| // tab) to add itself to the user's passkey-protected backup. Passkeys are bound |
| // to THIS origin, so the instance can't run the ceremony — it hands us the |
| // server entry (its URL + the user's session) in our URL FRAGMENT, which never |
diff --git a/internal/bot/eleven_test.go b/internal/bot/eleven_test.go| index 4e628df..f4e5213 100644 |
| --- a/internal/bot/eleven_test.go |
| +++ b/internal/bot/eleven_test.go |
| @@ -45,12 +45,12 @@ func TestElevenKnowledge(t *testing.T) { |
| } |
| } |
| forbidden := map[string]string{ |
| - `\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`: "an IP address", |
| - `(?i)luan\.chat|run\.garden|elevenmessenger\.com|paulca`: "a real hostname/instance", |
| - `(?i)hetzner|\bmiren\b|dnsimple|1password`: "an operator/tooling name", |
| - `(?i)\.p8\b|admintok|LCHAT_ADMIN_TOKEN|Bearer\s+\w`: "a key/token", |
| - `(?i)BEGIN (?:EC |RSA |OPENSSH )?PRIVATE KEY`: "a private key", |
| - `(?i)ssh\s+root@|root@\d`: "an SSH target", |
| + `\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`: "an IP address", |
| + `(?i)run\.garden|elevenmessenger\.com|paulca`: "a real hostname/instance", |
| + `(?i)hetzner|\bmiren\b|dnsimple|1password`: "an operator/tooling name", |
| + `(?i)\.p8\b|admintok|LCHAT_ADMIN_TOKEN|Bearer\s+\w`: "a key/token", |
| + `(?i)BEGIN (?:EC |RSA |OPENSSH )?PRIVATE KEY`: "a private key", |
| + `(?i)ssh\s+root@|root@\d`: "an SSH target", |
| } |
| for pat, what := range forbidden { |
| if regexp.MustCompile(pat).MatchString(appKnowledge) { |
diff --git a/ios/LChat/LChat.entitlements b/ios/LChat/LChat.entitlements| index bd54bab..9ec861f 100644 |
| --- a/ios/LChat/LChat.entitlements |
| +++ b/ios/LChat/LChat.entitlements |
| @@ -14,7 +14,7 @@ |
| opens the app directly (delivered to onOpenURL, fragment included). --> |
| <key>com.apple.developer.associated-domains</key> |
| <array> |
| - <string>applinks:*.luan.chat</string> |
| + <string>applinks:*.elevenmessenger.com</string> |
| </array> |
| <key>com.apple.security.application-groups</key> |
| <array> |
diff --git a/ios/LChatCore/Tests/LChatCoreTests/InviteLinkTests.swift b/ios/LChatCore/Tests/LChatCoreTests/InviteLinkTests.swift| index 16e4ea3..cfae44f 100644 |
| --- a/ios/LChatCore/Tests/LChatCoreTests/InviteLinkTests.swift |
| +++ b/ios/LChatCore/Tests/LChatCoreTests/InviteLinkTests.swift |
| @@ -6,20 +6,20 @@ import XCTest |
| // both the new "e1."-prefixed fragments and older unprefixed ones. |
| final class InviteLinkTests: XCTestCase { |
| func testParseJoinLink() { |
| - XCTAssertNotNil(parseJoinLink("https://family.luan.chat/join/join#e1.abc123"), |
| + XCTAssertNotNil(parseJoinLink("https://family.example.com/join/join#e1.abc123"), |
| "e1.-prefixed fragment parses") |
| - XCTAssertNotNil(parseJoinLink("https://family.luan.chat/join/join#abc123"), |
| + XCTAssertNotNil(parseJoinLink("https://family.example.com/join/join#abc123"), |
| "unprefixed fragment parses (older links)") |
| - XCTAssertNotNil(parseJoinLink(" https://h.luan.chat/join/x#e1.T \n"), |
| + XCTAssertNotNil(parseJoinLink(" https://h.example.com/join/x#e1.T \n"), |
| "whitespace is trimmed") |
| XCTAssertEqual(parseJoinLink("http://192.168.1.5:8080/join/x#e1.T")?.origin, |
| "http://192.168.1.5:8080", |
| "origin keeps scheme/host/port, drops path+fragment") |
| - XCTAssertNil(parseJoinLink("https://h.luan.chat/join/x#k=legacykey"), |
| + XCTAssertNil(parseJoinLink("https://h.example.com/join/x#k=legacykey"), |
| "legacy #k= links are rejected") |
| - XCTAssertNil(parseJoinLink("https://h.luan.chat/join/x"), |
| + XCTAssertNil(parseJoinLink("https://h.example.com/join/x"), |
| "missing fragment is rejected") |
| - XCTAssertNil(parseJoinLink("https://h.luan.chat/other/x#e1.T"), |
| + XCTAssertNil(parseJoinLink("https://h.example.com/other/x#e1.T"), |
| "non-/join/ path is rejected") |
| XCTAssertNil(parseJoinLink("elevenmessenger://add?url=x"), |
| "custom scheme is not itself a join link") |
| @@ -27,9 +27,9 @@ final class InviteLinkTests: XCTestCase { |
| func testInviteLinkFromOpenedURL() { |
| // The welcome page's custom-scheme form. |
| - let encoded = "https%3A%2F%2Ffamily.luan.chat%2Fjoin%2Fjoin%23e1.abc123" |
| + let encoded = "https%3A%2F%2Ffamily.example.com%2Fjoin%2Fjoin%23e1.abc123" |
| XCTAssertEqual(opened("elevenmessenger://add?url=\(encoded)"), |
| - "https://family.luan.chat/join/join#e1.abc123", |
| + "https://family.example.com/join/join#e1.abc123", |
| "elevenmessenger://add unwraps + percent-decodes the url param") |
| XCTAssertNil(opened("elevenmessenger://add"), |
| "custom scheme without a url param is rejected") |
| @@ -38,8 +38,8 @@ final class InviteLinkTests: XCTestCase { |
| // …and the universal-link form: the https invite passes straight through, |
| // fragment intact. |
| - XCTAssertEqual(opened("https://family.luan.chat/join/join#e1.abc123"), |
| - "https://family.luan.chat/join/join#e1.abc123", |
| + XCTAssertEqual(opened("https://family.example.com/join/join#e1.abc123"), |
| + "https://family.example.com/join/join#e1.abc123", |
| "https universal link passes through with its fragment") |
| XCTAssertNil(opened("ftp://h/join/x#e1.T"), "other schemes are rejected") |
| } |
diff --git a/push.go b/push.go| index e18b6be..5effa73 100644 |
| --- a/push.go |
| +++ b/push.go |
| @@ -281,7 +281,7 @@ func (s *Server) sendOne(payload []byte, kind string, sub PushSub) bool { |
| HTTPClient: pushClient, |
| // Pass the bare e-mail: webpush-go prepends "mailto:" itself. Passing |
| // "mailto:…" yields "mailto:mailto:…", which Apple rejects (BadJwtToken). |
| - Subscriber: "admin@luan.chat", |
| + Subscriber: "admin@elevenmessenger.com", |
| VAPIDPublicKey: s.vapidPub, |
| VAPIDPrivateKey: s.vapidPriv, |
| TTL: 60, |
diff --git a/push/db.go b/push/db.go| index 558c6b9..3834d4c 100644 |
| --- a/push/db.go |
| +++ b/push/db.go |
| @@ -44,7 +44,7 @@ func (db *DB) migrate() error { |
| _, err := db.sql.Exec(` |
| CREATE TABLE IF NOT EXISTS relay_tokens ( |
| id TEXT PRIMARY KEY, -- random public handle, used to revoke |
| - label TEXT NOT NULL, -- e.g. "family.luan.chat" — for logs/listing, not auth |
| + label TEXT NOT NULL, -- e.g. "family.example.com" — for logs/listing, not auth |
| token_hash TEXT NOT NULL UNIQUE, -- sha256(bearer token); the secret itself is never stored |
| created_at INTEGER NOT NULL, |
| last_used_at INTEGER |
diff --git a/push/push_test.go b/push/push_test.go| index b04d4ce..12977ce 100644 |
| --- a/push/push_test.go |
| +++ b/push/push_test.go |
| @@ -114,7 +114,7 @@ func TestPushAuth(t *testing.T) { |
| if got := push(t, base, "bogus-token", liveDevice, ""); got != http.StatusUnauthorized { |
| t.Errorf("bad token: want 401, got %d", got) |
| } |
| - _, tok := mint(t, base, "family.luan.chat") |
| + _, tok := mint(t, base, "family.example.com") |
| if got := push(t, base, tok, liveDevice, ""); got != http.StatusOK { |
| t.Errorf("minted token: want 200, got %d", got) |
| } |
| @@ -122,7 +122,7 @@ func TestPushAuth(t *testing.T) { |
| func TestPushRevoke(t *testing.T) { |
| base := newTestRelay(t).URL |
| - id, tok := mint(t, base, "test.luan.chat") |
| + id, tok := mint(t, base, "test.example.com") |
| if got := push(t, base, tok, liveDevice, ""); got != http.StatusOK { |
| t.Fatalf("pre-revoke: want 200, got %d", got) |
| } |
| @@ -138,7 +138,7 @@ func TestPushRevoke(t *testing.T) { |
| func TestPushGonePassthrough(t *testing.T) { |
| base := newTestRelay(t).URL |
| - _, tok := mint(t, base, "test.luan.chat") |
| + _, tok := mint(t, base, "test.example.com") |
| // Apple says the token is dead → relay must return a bare 410 so the |
| // instance prunes it. |
| if got := push(t, base, tok, deadDevice, ""); got != http.StatusGone { |
| @@ -148,7 +148,7 @@ func TestPushGonePassthrough(t *testing.T) { |
| func TestPushBadDeviceToken(t *testing.T) { |
| base := newTestRelay(t).URL |
| - _, tok := mint(t, base, "test.luan.chat") |
| + _, tok := mint(t, base, "test.example.com") |
| if got := push(t, base, tok, "zznothex", ""); got != http.StatusBadRequest { |
| t.Errorf("non-hex device token: want 400, got %d", got) |
| } |
| @@ -156,7 +156,7 @@ func TestPushBadDeviceToken(t *testing.T) { |
| func TestPushOversized(t *testing.T) { |
| base := newTestRelay(t).URL |
| - _, tok := mint(t, base, "test.luan.chat") |
| + _, tok := mint(t, base, "test.example.com") |
| // A blob well over maxPushBytes must 413, never 410 (a false prune). |
| big := `,"pad":"` + strings.Repeat("a", 9000) + `"` |
| if got := push(t, base, tok, liveDevice, big); got != http.StatusRequestEntityTooLarge { |
diff --git a/test/knowledge.test.mjs b/test/knowledge.test.mjs| index 2d5f5f9..c3a5aea 100644 |
| --- a/test/knowledge.test.mjs |
| +++ b/test/knowledge.test.mjs |
| @@ -29,7 +29,7 @@ test("the knowledge base is substantial (not accidentally emptied)", () => { |
| test("the knowledge base leaks NO operational secrets", () => { |
| const forbidden = [ |
| [/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, "an IP address"], |
| - [/luan\.chat|run\.garden|elevenmessenger\.com|paulca/i, "a real hostname/instance"], |
| + [/run\.garden|elevenmessenger\.com|paulca/i, "a real hostname/instance"], |
| [/hetzner|\bmiren\b|dnsimple|1password|\bop\b\s+(?:read|item)/i, "an operator/tooling name"], |
| [/\.p8\b|J5NDF5|ZKYQ9|9X6NKTF|admintok|LCHAT_ADMIN_TOKEN|Bearer\s+\w/i, "a key/token"], |
| [/BEGIN (?:EC |RSA |OPENSSH )?PRIVATE KEY/i, "a private key"], |
diff --git a/unfurl.go b/unfurl.go| index 56f39a3..592800e 100644 |
| --- a/unfurl.go |
| +++ b/unfurl.go |
| @@ -108,7 +108,7 @@ func fetchOG(rawURL string) (LinkPreview, error) { |
| if err != nil { |
| return pv, err |
| } |
| - req.Header.Set("User-Agent", "lchat-linkpreview/1.0 (+https://luan.chat)") |
| + req.Header.Set("User-Agent", "lchat-linkpreview/1.0 (+https://elevenmessenger.com)") |
| req.Header.Set("Accept", "text/html,application/xhtml+xml") |
| resp, err := ogClient.Do(req) |
diff --git a/versions_test.go b/versions_test.go| index dede662..a78242a 100644 |
| --- a/versions_test.go |
| +++ b/versions_test.go |
| @@ -62,10 +62,10 @@ func TestStageAndPointLive(t *testing.T) { |
| // Pointing an instance's live link resolves to the staged binary and reports |
| // the right version. |
| - if err := pointLive("test.luan.chat", "abc123"); err != nil { |
| + if err := pointLive("test.example.com", "abc123"); err != nil { |
| t.Fatalf("pointLive: %v", err) |
| } |
| - got, err := liveVersion("test.luan.chat") |
| + got, err := liveVersion("test.example.com") |
| if err != nil { |
| t.Fatalf("liveVersion: %v", err) |
| } |
| @@ -73,7 +73,7 @@ func TestStageAndPointLive(t *testing.T) { |
| t.Errorf("liveVersion = %q, want abc123", got) |
| } |
| // The symlink actually points at the version binary. |
| - resolved, err := filepath.EvalSymlinks(liveLink("test.luan.chat")) |
| + resolved, err := filepath.EvalSymlinks(liveLink("test.example.com")) |
| if err != nil { |
| t.Fatalf("EvalSymlinks: %v", err) |
| } |
| @@ -84,7 +84,7 @@ func TestStageAndPointLive(t *testing.T) { |
| func TestPointLiveUnstagedFails(t *testing.T) { |
| withTempOpt(t) |
| - if err := pointLive("test.luan.chat", "nope"); err == nil { |
| + if err := pointLive("test.example.com", "nope"); err == nil { |
| t.Error("pointLive to an unstaged version should fail, but succeeded") |
| } |
| } |