Skip to content

Commit 674ad40

Browse files
committed
feat(cloud-shared): rebrand-ready agent base domain config
Three coordinated changes prepare the cloud-shared layer for the in-flight rebrand from waifu.fun / milady.ai / shad0w.xyz to elizacloud.ai, all gated on env vars so the runtime impact is zero until ELIZA_CLOUD_AGENT_BASE_DOMAIN flips in deployment env: 1. `DOMAIN_ALIAS_GROUPS` extended from the original [waifu.fun, eliza.ai] pair to the full 5-domain group [waifu.fun, eliza.ai, elizacloud.ai, milady.ai, shad0w.xyz]. The pairing-token CORS Origin check now tries every alternate, so a token issued against `<uuid>.waifu.fun` validates when the user reaches `<uuid>.elizacloud.ai` (or any other group member). The alias resolver is extracted into a standalone pure module `pairing-token-domains.ts` so it can be unit-tested without dragging in the Postgres repository import chain. The DB-bound service in `pairing-token.ts` re-exports the same names for back compat with existing callers. 2. `DEFAULT_AGENT_BASE_DOMAIN` in `eliza-agent-web-ui.ts` flips from "waifu.fun" to "elizacloud.ai" so a fresh deploy without `ELIZA_CLOUD_AGENT_BASE_DOMAIN` set picks the rebrand brand by default. Existing deployments override via the env var (currently set to "milady.ai" on the prod manager VM) — unchanged. 3. Stale comment `Public URL: https://{agentId}.waifu.fun/...` in eliza-sandbox.ts replaced with the env-var-templated equivalent so the comment ages with the deployment, not against it. Tests: 8 sociable specs in `pairing-token.test.ts` cover the alias matrix (suffix anchoring, UUID prefix preservation, port round-trip, unparseable input, group membership guarantee). bun test 8/8 pass.
1 parent 4694935 commit 674ad40

5 files changed

Lines changed: 153 additions & 37 deletions

File tree

packages/cloud-shared/src/lib/eliza-agent-web-ui.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AgentSandbox } from "../db/schemas/agent-sandboxes";
22

3-
const DEFAULT_AGENT_BASE_DOMAIN = "waifu.fun";
3+
const DEFAULT_AGENT_BASE_DOMAIN = "elizacloud.ai";
44

55
type ElizaAgentWebUiTarget = Pick<
66
AgentSandbox,
@@ -16,7 +16,7 @@ export interface ElizaAgentWebUiUrlOptions {
1616
path?: string;
1717
}
1818

19-
/** Resolved base domain for the current deployment (e.g. "waifu.fun"). */
19+
/** Resolved base domain for the current deployment (e.g. "elizacloud.ai"). */
2020
export function getAgentBaseDomain(): string {
2121
return (
2222
normalizeAgentBaseDomain(process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN) ?? DEFAULT_AGENT_BASE_DOMAIN
@@ -59,7 +59,7 @@ function applyPath(baseUrl: string, path = "/"): string {
5959
* Public HTTPS URL `{sandbox.id}.{domain}`.
6060
*
6161
* **Omit `baseDomain` or set it to `undefined`:** resolve from `ELIZA_CLOUD_AGENT_BASE_DOMAIN`,
62-
* then the built-in default domain (`waifu.fun`). Empty env is treated like unset (same as
62+
* then the built-in default domain (`elizacloud.ai`). Empty env is treated like unset (same as
6363
* {@link getAgentBaseDomain}).
6464
*
6565
* **Pass any other `baseDomain` (including `null` or `""`):** use only that value after

packages/cloud-shared/src/lib/services/eliza-sandbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2187,7 +2187,7 @@ export class ElizaSandboxService {
21872187
const agentBaseDomain = process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN;
21882188
let endpoint: string;
21892189
if (agentBaseDomain) {
2190-
// Public URL: https://{agentId}.waifu.fun/api/wallet/...
2190+
// Public URL: https://{agentId}.{ELIZA_CLOUD_AGENT_BASE_DOMAIN}/api/wallet/...
21912191
endpoint = `https://${agentId}.${agentBaseDomain}${fullPath}`;
21922192
} else if (rec.web_ui_port && rec.node_id) {
21932193
// Internal fallback: http://{host}:{web_ui_port}/api/wallet/...
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Domain alias groups — all domains in a group resolve to the same agent
2+
// container. Each agent's public URL may be rewritten between any two
3+
// domains in the same group (e.g. the dashboard generates an
4+
// `<uuid>.elizacloud.ai` link but the agent was originally provisioned with
5+
// an `<uuid>.waifu.fun` Origin), so token validation tries every alias.
6+
//
7+
// `.elizacloud.ai` is the canonical post-2026-05 brand; the others are
8+
// kept during the rebrand grace period and can be retired one by one once
9+
// no DB rows reference them.
10+
//
11+
// Pure data + pure function — extracted from `pairing-token.ts` so the
12+
// alias logic stays unit-testable without pulling the Postgres repository
13+
// import chain.
14+
15+
export const DOMAIN_ALIAS_GROUPS: readonly (readonly string[])[] = [
16+
[".waifu.fun", ".eliza.ai", ".elizacloud.ai", ".milady.ai", ".shad0w.xyz"],
17+
];
18+
19+
/**
20+
* Given an origin like https://uuid.waifu.fun, return every other origin
21+
* that resolves to the same agent container under
22+
* {@link DOMAIN_ALIAS_GROUPS}. Empty array if the origin's hostname does
23+
* not match any aliased suffix, or if the input is not a parseable URL.
24+
*/
25+
export function getAlternateDomainOrigins(origin: string): string[] {
26+
let url: URL;
27+
try {
28+
url = new URL(origin);
29+
} catch {
30+
return [];
31+
}
32+
for (const group of DOMAIN_ALIAS_GROUPS) {
33+
const matched = group.find((suffix) => url.hostname.endsWith(suffix));
34+
if (!matched) continue;
35+
const alternates: string[] = [];
36+
for (const candidate of group) {
37+
if (candidate === matched) continue;
38+
const altUrl = new URL(url.toString());
39+
altUrl.hostname = url.hostname.replace(
40+
new RegExp(`${matched.replaceAll(".", "\\.")}$`),
41+
candidate,
42+
);
43+
alternates.push(altUrl.origin);
44+
}
45+
return alternates;
46+
}
47+
return [];
48+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { DOMAIN_ALIAS_GROUPS, getAlternateDomainOrigins } from "./pairing-token-domains";
3+
4+
describe("getAlternateDomainOrigins", () => {
5+
it("returns every other suffix in the same alias group", () => {
6+
// The canonical group is the first entry. Verify all five domains
7+
// produce four alternates each (the matched suffix is excluded).
8+
const inputs = [
9+
"https://abc.waifu.fun",
10+
"https://abc.eliza.ai",
11+
"https://abc.elizacloud.ai",
12+
"https://abc.milady.ai",
13+
"https://abc.shad0w.xyz",
14+
];
15+
16+
for (const origin of inputs) {
17+
const alts = getAlternateDomainOrigins(origin);
18+
expect(alts).toHaveLength(4);
19+
expect(alts).not.toContain(origin);
20+
const hostnames = alts.map((url) => new URL(url).hostname);
21+
for (const hostname of hostnames) {
22+
expect(hostname.startsWith("abc.")).toBe(true);
23+
}
24+
}
25+
});
26+
27+
it("rewrites the suffix while keeping the agent UUID prefix intact", () => {
28+
const alts = getAlternateDomainOrigins(
29+
"https://9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.waifu.fun",
30+
);
31+
const hostnames = alts.map((u) => new URL(u).hostname).sort();
32+
expect(hostnames).toEqual(
33+
[
34+
"9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.eliza.ai",
35+
"9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.elizacloud.ai",
36+
"9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.milady.ai",
37+
"9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.shad0w.xyz",
38+
].sort(),
39+
);
40+
});
41+
42+
it("preserves the URL port when an origin includes one", () => {
43+
// `URL.origin` keeps non-default ports — the alternate origins must
44+
// round-trip them so a sandbox served on :8443 still matches its alias.
45+
const alts = getAlternateDomainOrigins("https://abc.waifu.fun:8443");
46+
expect(alts.length).toBeGreaterThan(0);
47+
for (const alt of alts) {
48+
const url = new URL(alt);
49+
expect(url.port).toBe("8443");
50+
}
51+
});
52+
53+
it("returns an empty array when no aliased suffix matches", () => {
54+
expect(getAlternateDomainOrigins("https://example.com")).toEqual([]);
55+
expect(getAlternateDomainOrigins("https://app.elizacloud.io")).toEqual([]);
56+
expect(getAlternateDomainOrigins("https://waifu.fun.evil.tld")).toEqual([]);
57+
});
58+
59+
it("returns an empty array for unparseable input rather than throwing", () => {
60+
expect(getAlternateDomainOrigins("not a url")).toEqual([]);
61+
expect(getAlternateDomainOrigins("")).toEqual([]);
62+
expect(getAlternateDomainOrigins("://no-protocol")).toEqual([]);
63+
});
64+
65+
it("matches the suffix on the right boundary (no partial-domain false positive)", () => {
66+
// `notwaifu.fun` contains the literal text `waifu.fun` but does not end
67+
// with `.waifu.fun`, so it must not alias into the group.
68+
expect(getAlternateDomainOrigins("https://abc.notwaifu.fun")).toEqual([]);
69+
expect(getAlternateDomainOrigins("https://abceliza.ai")).toEqual([]);
70+
});
71+
});
72+
73+
describe("DOMAIN_ALIAS_GROUPS", () => {
74+
it("declares the rebrand-target domain `.elizacloud.ai` so the suffix matches", () => {
75+
// This is the load-bearing guarantee for the rebrand: pairing tokens
76+
// issued against `.waifu.fun` must validate when the dashboard rewrites
77+
// the agent URL to `.elizacloud.ai`. If someone removes elizacloud.ai
78+
// from the group, this test fails loudly.
79+
const allDomains = DOMAIN_ALIAS_GROUPS.flat();
80+
expect(allDomains).toContain(".elizacloud.ai");
81+
expect(allDomains).toContain(".waifu.fun");
82+
});
83+
84+
it("uses leading-dot suffixes so subdomain matching is anchored", () => {
85+
for (const group of DOMAIN_ALIAS_GROUPS) {
86+
for (const suffix of group) {
87+
expect(suffix.startsWith(".")).toBe(true);
88+
}
89+
}
90+
});
91+
});

packages/cloud-shared/src/lib/services/pairing-token.ts

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { agentPairingTokensRepository } from "../../db/repositories/agent-pairing-tokens";
2+
import { DOMAIN_ALIAS_GROUPS, getAlternateDomainOrigins } from "./pairing-token-domains";
3+
4+
export { DOMAIN_ALIAS_GROUPS, getAlternateDomainOrigins };
25

36
interface PairingToken {
47
userId: string;
@@ -47,35 +50,7 @@ function createPairingToken(): string {
4750
return base64UrlEncode(bytes);
4851
}
4952

50-
// Domain aliases — waifu.fun and eliza.ai resolve to the same containers.
51-
// The dashboard rewrites URLs from one to the other, so the Origin header
52-
// sent by pair.html may use either domain.
53-
const DOMAIN_ALIASES: [string, string][] = [[".waifu.fun", ".eliza.ai"]];
54-
5553
class PairingTokenService {
56-
/**
57-
* Given an origin like https://uuid.waifu.fun, return https://uuid.eliza.ai
58-
* (and vice versa). Returns null if no alias applies.
59-
*/
60-
private getAlternateDomainOrigin(origin: string): string | null {
61-
for (const [a, b] of DOMAIN_ALIASES) {
62-
try {
63-
const url = new URL(origin);
64-
if (url.hostname.endsWith(a)) {
65-
url.hostname = url.hostname.replace(new RegExp(`${a.replaceAll(".", "\\.")}$`), b);
66-
return url.origin;
67-
}
68-
if (url.hostname.endsWith(b)) {
69-
url.hostname = url.hostname.replace(new RegExp(`${b.replaceAll(".", "\\.")}$`), a);
70-
return url.origin;
71-
}
72-
} catch {
73-
// Invalid URL — skip
74-
}
75-
}
76-
return null;
77-
}
78-
7954
async generateToken(
8055
userId: string,
8156
orgId: string,
@@ -117,16 +92,18 @@ class PairingTokenService {
11792
normalizedOrigin,
11893
);
11994

120-
// If no match, try the alternate domain. The dashboard may rewrite
121-
// waifu.fun → eliza.ai (or vice versa) which changes the Origin header
122-
// but both domains resolve to the same agent container.
95+
// If no match, try each alternate domain in the same alias group. The
96+
// dashboard may rewrite the agent URL between any two aliased domains
97+
// (waifu.fun ↔ eliza.ai ↔ elizacloud.ai ↔ milady.ai ↔ shad0w.xyz), and
98+
// we cannot predict which one is stored as `expected_origin` for a
99+
// given token row.
123100
if (!row) {
124-
const alternateOrigin = this.getAlternateDomainOrigin(normalizedOrigin);
125-
if (alternateOrigin) {
101+
for (const alternateOrigin of getAlternateDomainOrigins(normalizedOrigin)) {
126102
row = await agentPairingTokensRepository.consumeValidToken(
127103
await hashToken(token),
128104
alternateOrigin,
129105
);
106+
if (row) break;
130107
}
131108
}
132109

0 commit comments

Comments
 (0)