Skip to content

Commit 1fa8f23

Browse files
hotlongCopilot
andcommitted
feat(objectos): 404 unknown hostnames instead of serving control-plane console
Without a host-resolution guard, unknown subdomains like 'demo-xxx.objectos.app' rendered the same console UI as a real env because the static Console SPA is served independently of the request host. The user could not tell the env did not exist. Add a request-level middleware in apps/objectos/server/index.ts that: - Activates when OS_ROOT_DOMAIN is set (e.g. 'objectos.app'). - For any '*.OS_ROOT_DOMAIN' host that is not a reserved platform subdomain (cloud/www/api/docs/admin/app) and not an infra path (/_admin/*, /.well-known/*), looks up the env via envRegistry. - If no env claims that hostname, returns 404 { error: 'environment_not_found', hostname }. - Falls through if registry is unavailable or for non-platform hosts so custom domains and local dev keep working. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9f87aa8 commit 1fa8f23

2 files changed

Lines changed: 72 additions & 1 deletion

File tree

apps/objectos/cloudflare/worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// when shipping a hotfix that lives inside the Container image. The
55
// DO only reloads when worker code changes, so a no-op edit here
66
// guarantees the container restarts with the freshly-pushed image.
7-
// build: 2026-05-20T16:00Z org-scoped-overlay-refactor
7+
// build: 2026-05-20T16:30Z unknown-hostname-404
88

99
/**
1010
* Cloudflare Containers entrypoint for ObjectOS.

apps/objectos/server/index.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,77 @@ async function bootKernel(): Promise<BootResult> {
5555
kernel.registerService('http.server', httpServer);
5656
kernel.registerService('http-server', httpServer); // alias for backward compatibility
5757

58+
// Unknown-environment hostname guard.
59+
//
60+
// In multi-tenant cloud deployments (objectos.app), every public
61+
// hostname is expected to map to a `sys_environment` row whose
62+
// hostname column matches the request `Host`. Without this guard,
63+
// an unknown subdomain like `demo-xxx.objectos.app` happily renders
64+
// the control-plane console (because the console SPA is served
65+
// statically and ignores the host), making the deployment look like
66+
// it has data when it doesn't. We respond with a clear 404 instead.
67+
//
68+
// Activation: only when OS_ROOT_DOMAIN is set (e.g. "objectos.app").
69+
// Reserved subdomains (cloud/www/api/docs/admin) bypass the check so
70+
// the platform's own surfaces and infra endpoints keep working.
71+
// Custom domains that aren't subdomains of the root are passed
72+
// through unchanged — a tenant's bring-your-own-domain still needs
73+
// to be looked up via envRegistry, but a miss there falls back to
74+
// the legacy behaviour to avoid blocking unknown-yet-valid hosts.
75+
const rootDomain = (process.env.OS_ROOT_DOMAIN || '').trim().toLowerCase();
76+
if (rootDomain) {
77+
const RESERVED = new Set(['', 'cloud', 'www', 'api', 'docs', 'admin', 'app']);
78+
const rawApp = httpServer.getRawApp();
79+
let envRegistryRef: any;
80+
const getEnvRegistry = async () => {
81+
if (envRegistryRef !== undefined) return envRegistryRef;
82+
try {
83+
envRegistryRef = await (kernel as any).getServiceAsync?.('env-registry') ?? null;
84+
} catch {
85+
envRegistryRef = null;
86+
}
87+
return envRegistryRef;
88+
};
89+
rawApp.use('*', async (c: any, next: any) => {
90+
const rawHost = c.req.header('host') || '';
91+
const host = rawHost.split(':')[0].toLowerCase();
92+
if (!host) return next();
93+
const isPlatformHost = host === rootDomain || host.endsWith('.' + rootDomain);
94+
if (!isPlatformHost) return next();
95+
const sub = host === rootDomain ? '' : host.slice(0, -(rootDomain.length + 1));
96+
// Treat any reserved subdomain (and apex) as platform infra,
97+
// not a tenant env. Also allow nested platform prefixes like
98+
// `api.cloud.objectos.app`.
99+
const head = sub.split('.').pop() || '';
100+
if (RESERVED.has(sub) || RESERVED.has(head)) return next();
101+
// Always allow platform-level infra endpoints regardless of host.
102+
const p = c.req.path;
103+
if (p.startsWith('/_admin/') || p === '/_admin' || p.startsWith('/.well-known/')) {
104+
return next();
105+
}
106+
const registry = await getEnvRegistry();
107+
if (!registry || typeof registry.resolveByHostname !== 'function') {
108+
// Registry unavailable — don't synthesize a 404 (could be
109+
// a boot-time race or a non-cloud deployment).
110+
return next();
111+
}
112+
try {
113+
const hit = await registry.resolveByHostname(host);
114+
if (hit) return next();
115+
} catch {
116+
return next();
117+
}
118+
return c.json(
119+
{
120+
error: 'environment_not_found',
121+
message: `No environment is bound to hostname '${host}'.`,
122+
hostname: host,
123+
},
124+
404,
125+
);
126+
});
127+
}
128+
58129
// 1. Config plugins (control-plane preset + MultiProjectPlugin + Auth/Security/Audit).
59130
// AuthPlugin registers the platform Setup App via its manifest
60131
// (definition lives in @objectstack/platform-objects/apps).

0 commit comments

Comments
 (0)