Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/cloud-shared/src/lib/eliza-agent-web-ui.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AgentSandbox } from "../db/schemas/agent-sandboxes";

const DEFAULT_AGENT_BASE_DOMAIN = "waifu.fun";
const DEFAULT_AGENT_BASE_DOMAIN = "elizacloud.ai";

type ElizaAgentWebUiTarget = Pick<
AgentSandbox,
Expand All @@ -16,7 +16,7 @@ export interface ElizaAgentWebUiUrlOptions {
path?: string;
}

/** Resolved base domain for the current deployment (e.g. "waifu.fun"). */
/** Resolved base domain for the current deployment (e.g. "elizacloud.ai"). */
export function getAgentBaseDomain(): string {
return (
normalizeAgentBaseDomain(process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN) ?? DEFAULT_AGENT_BASE_DOMAIN
Expand Down Expand Up @@ -59,7 +59,7 @@ function applyPath(baseUrl: string, path = "/"): string {
* Public HTTPS URL `{sandbox.id}.{domain}`.
*
* **Omit `baseDomain` or set it to `undefined`:** resolve from `ELIZA_CLOUD_AGENT_BASE_DOMAIN`,
* then the built-in default domain (`waifu.fun`). Empty env is treated like unset (same as
* then the built-in default domain (`elizacloud.ai`). Empty env is treated like unset (same as
* {@link getAgentBaseDomain}).
*
* **Pass any other `baseDomain` (including `null` or `""`):** use only that value after
Expand Down
2 changes: 1 addition & 1 deletion packages/cloud-shared/src/lib/services/eliza-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2187,7 +2187,7 @@ export class ElizaSandboxService {
const agentBaseDomain = process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN;
let endpoint: string;
if (agentBaseDomain) {
// Public URL: https://{agentId}.waifu.fun/api/wallet/...
// Public URL: https://{agentId}.{ELIZA_CLOUD_AGENT_BASE_DOMAIN}/api/wallet/...
endpoint = `https://${agentId}.${agentBaseDomain}${fullPath}`;
} else if (rec.web_ui_port && rec.node_id) {
// Internal fallback: http://{host}:{web_ui_port}/api/wallet/...
Expand Down
54 changes: 54 additions & 0 deletions packages/cloud-shared/src/lib/services/pairing-token-domains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Domain alias groups — all domains in a group resolve to the same agent
// container. Each agent's public URL may be rewritten between any two
// domains in the same group (e.g. the dashboard generates an
// `<uuid>.elizacloud.ai` link but the agent was originally provisioned with
// an `<uuid>.waifu.fun` Origin), so token validation tries every alias.
//
// `.elizacloud.ai` is the canonical post-2026-05 brand; `.waifu.fun` and
// `.eliza.ai` are kept during the rebrand grace period and can be retired
// once no DB rows reference them.
//
// Intentionally NOT in this list:
// - `.milady.ai` — resolves to GitHub Pages (static front, no agent
// sandbox infra was ever served from this domain)
// - `.shad0w.xyz` — personal handle from the 0xSolace stack, never
// served real production sandbox URLs
// Both are part of the "0 legacy" cleanup goal; old bookmarks under those
// domains will fail Origin validation, which is the intended outcome.
//
// Pure data + pure function — extracted from `pairing-token.ts` so the
// alias logic stays unit-testable without pulling the Postgres repository
// import chain.

export const DOMAIN_ALIAS_GROUPS: readonly (readonly string[])[] = [
[".waifu.fun", ".eliza.ai", ".elizacloud.ai"],
];

/**
* Given an origin like https://uuid.waifu.fun, return every other origin
* that resolves to the same agent container under
* {@link DOMAIN_ALIAS_GROUPS}. Empty array if the origin's hostname does
* not match any aliased suffix, or if the input is not a parseable URL.
*/
export function getAlternateDomainOrigins(origin: string): string[] {
let url: URL;
try {
url = new URL(origin);
} catch {
return [];
}
for (const group of DOMAIN_ALIAS_GROUPS) {
const matched = group.find((suffix) => url.hostname.endsWith(suffix));
if (!matched) continue;
const prefix = url.hostname.slice(0, -matched.length);
const alternates: string[] = [];
for (const candidate of group) {
if (candidate === matched) continue;
const altUrl = new URL(url);
altUrl.hostname = prefix + candidate;
alternates.push(altUrl.origin);
}
return alternates;
}
return [];
}
109 changes: 109 additions & 0 deletions packages/cloud-shared/src/lib/services/pairing-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, expect, it } from "bun:test";
import { DOMAIN_ALIAS_GROUPS, getAlternateDomainOrigins } from "./pairing-token-domains";

describe("getAlternateDomainOrigins", () => {
it("returns every other suffix in the same alias group", () => {
// The canonical group is the first entry. Verify each domain produces
// (group.length - 1) alternates — the matched suffix is excluded.
const inputs = ["https://abc.waifu.fun", "https://abc.eliza.ai", "https://abc.elizacloud.ai"];

for (const origin of inputs) {
const alts = getAlternateDomainOrigins(origin);
expect(alts).toHaveLength(2);
expect(alts).not.toContain(origin);
const hostnames = alts.map((url) => new URL(url).hostname);
for (const hostname of hostnames) {
expect(hostname.startsWith("abc.")).toBe(true);
}
}
});

it("rewrites the suffix while keeping the agent UUID prefix intact", () => {
const alts = getAlternateDomainOrigins(
"https://9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.waifu.fun",
);
const hostnames = alts.map((u) => new URL(u).hostname).sort();
expect(hostnames).toEqual(
[
"9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.eliza.ai",
"9d77d8b5-1d63-4b4c-9bd1-ec1b5deb4dc8.elizacloud.ai",
].sort(),
);
});

it("rejects retired 0xSolace-era domains (milady.ai, shad0w.xyz)", () => {
// These domains were intentionally dropped from the alias group to
// close the "0 legacy" cleanup. A leftover bookmark must fail Origin
// validation rather than silently aliasing into a live brand.
expect(getAlternateDomainOrigins("https://abc.milady.ai")).toEqual([]);
expect(getAlternateDomainOrigins("https://abc.shad0w.xyz")).toEqual([]);
});

it("preserves the URL port when an origin includes one", () => {
// `URL.origin` keeps non-default ports — the alternate origins must
// round-trip them so a sandbox served on :8443 still matches its alias.
const alts = getAlternateDomainOrigins("https://abc.waifu.fun:8443");
expect(alts).toHaveLength(2);
for (const alt of alts) {
const url = new URL(alt);
expect(url.port).toBe("8443");
}
});

it("rewrites only the suffix when the prefix is itself a multi-level subdomain", () => {
// Production sandbox URLs are flat (`<uuid>.waifu.fun`), but the
// suffix-strip algorithm should treat anything before the matched
// suffix as opaque prefix — so `a.b.c.waifu.fun` aliases to
// `a.b.c.eliza.ai` without touching the inner labels.
const alts = getAlternateDomainOrigins("https://a.b.c.waifu.fun");
const hostnames = alts.map((u) => new URL(u).hostname).sort();
expect(hostnames).toEqual(["a.b.c.eliza.ai", "a.b.c.elizacloud.ai"].sort());
});

it("returns an empty array when no aliased suffix matches", () => {
expect(getAlternateDomainOrigins("https://example.com")).toEqual([]);
expect(getAlternateDomainOrigins("https://app.elizacloud.io")).toEqual([]);
expect(getAlternateDomainOrigins("https://waifu.fun.evil.tld")).toEqual([]);
});

it("returns an empty array for unparseable input rather than throwing", () => {
expect(getAlternateDomainOrigins("not a url")).toEqual([]);
expect(getAlternateDomainOrigins("")).toEqual([]);
expect(getAlternateDomainOrigins("://no-protocol")).toEqual([]);
});

it("matches uppercase hostnames (URL parser lowercases per WHATWG spec)", () => {
// `endsWith` is case-sensitive but `new URL()` lowercases the hostname,
// so an Origin header arriving as `https://ABC.WAIFU.FUN` still aliases.
const alts = getAlternateDomainOrigins("https://ABC.WAIFU.FUN");
const hostnames = alts.map((u) => new URL(u).hostname).sort();
expect(hostnames).toEqual(["abc.eliza.ai", "abc.elizacloud.ai"].sort());
});

it("matches the suffix on the right boundary (no partial-domain false positive)", () => {
// `notwaifu.fun` contains the literal text `waifu.fun` but does not end
// with `.waifu.fun`, so it must not alias into the group.
expect(getAlternateDomainOrigins("https://abc.notwaifu.fun")).toEqual([]);
expect(getAlternateDomainOrigins("https://abceliza.ai")).toEqual([]);
});
});

describe("DOMAIN_ALIAS_GROUPS", () => {
it("declares the rebrand-target domain `.elizacloud.ai` so the suffix matches", () => {
// This is the load-bearing guarantee for the rebrand: pairing tokens
// issued against `.waifu.fun` must validate when the dashboard rewrites
// the agent URL to `.elizacloud.ai`. If someone removes elizacloud.ai
// from the group, this test fails loudly.
const allDomains = DOMAIN_ALIAS_GROUPS.flat();
expect(allDomains).toContain(".elizacloud.ai");
expect(allDomains).toContain(".waifu.fun");
});

it("uses leading-dot suffixes so subdomain matching is anchored", () => {
for (const group of DOMAIN_ALIAS_GROUPS) {
for (const suffix of group) {
expect(suffix.startsWith(".")).toBe(true);
}
}
});
});
40 changes: 7 additions & 33 deletions packages/cloud-shared/src/lib/services/pairing-token.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { agentPairingTokensRepository } from "../../db/repositories/agent-pairing-tokens";
import { getAlternateDomainOrigins } from "./pairing-token-domains";

interface PairingToken {
userId: string;
Expand Down Expand Up @@ -47,35 +48,7 @@ function createPairingToken(): string {
return base64UrlEncode(bytes);
}

// Domain aliases — waifu.fun and eliza.ai resolve to the same containers.
// The dashboard rewrites URLs from one to the other, so the Origin header
// sent by pair.html may use either domain.
const DOMAIN_ALIASES: [string, string][] = [[".waifu.fun", ".eliza.ai"]];

class PairingTokenService {
/**
* Given an origin like https://uuid.waifu.fun, return https://uuid.eliza.ai
* (and vice versa). Returns null if no alias applies.
*/
private getAlternateDomainOrigin(origin: string): string | null {
for (const [a, b] of DOMAIN_ALIASES) {
try {
const url = new URL(origin);
if (url.hostname.endsWith(a)) {
url.hostname = url.hostname.replace(new RegExp(`${a.replaceAll(".", "\\.")}$`), b);
return url.origin;
}
if (url.hostname.endsWith(b)) {
url.hostname = url.hostname.replace(new RegExp(`${b.replaceAll(".", "\\.")}$`), a);
return url.origin;
}
} catch {
// Invalid URL — skip
}
}
return null;
}

async generateToken(
userId: string,
orgId: string,
Expand Down Expand Up @@ -117,16 +90,17 @@ class PairingTokenService {
normalizedOrigin,
);

// If no match, try the alternate domain. The dashboard may rewrite
// waifu.fun → eliza.ai (or vice versa) which changes the Origin header
// but both domains resolve to the same agent container.
// If no match, try each alternate domain in the same alias group. The
// dashboard may rewrite the agent URL between any two aliased domains
// (waifu.fun ↔ eliza.ai ↔ elizacloud.ai), and we cannot predict which
// one is stored as `expected_origin` for a given token row.
if (!row) {
const alternateOrigin = this.getAlternateDomainOrigin(normalizedOrigin);
if (alternateOrigin) {
for (const alternateOrigin of getAlternateDomainOrigins(normalizedOrigin)) {
row = await agentPairingTokensRepository.consumeValidToken(
await hashToken(token),
alternateOrigin,
);
if (row) break;
}
}

Expand Down
Loading