Merge PUT-2110 and PUT-2111 for the admin-v2 test deploy

This commit is contained in:
Juan Castro committed 2026-10-19 11:42:28 -04:00
commit b0e9fc955f
6 files changed
+191 -23

No files matched your search

@@ -176,7 +176,7 @@ describe('createUserSubdomainNotFound', () => {
it('passes through reserved subdomains (api, js, native apps, etc.)', () => {
// `api`, `js`, `dav`, `docs`, `developer`, `editor`, `pdf`,
// `puter-app-icons`, `onlyoffice`, etc. all bypass.
// `puter-app-icons`, etc. all pass through.
for (const sub of ['api', 'js', 'docs', 'editor', 'puter-app-icons']) {
const { out, next } = run(
createUserSubdomainNotFound(config),
@@ -190,6 +190,17 @@ describe('createUserSubdomainNotFound', () => {
}
});
it('404s an extension-owned subdomain nobody has claimed', () => {
const { next } = run(
createUserSubdomainNotFound(config),
makeReq({
subdomains: ['com', 'puter', 'onlyoffice'],
host: 'onlyoffice.puter.com',
}),
);
expectNotFound(next);
});
it('passes through when no subdomain is present (root)', () => {
const { next } = run(
createUserSubdomainNotFound(config),
@@ -641,6 +652,21 @@ describe('createUserSubdomainNotFound with claimed subdomains', () => {
expect(args).toEqual([]);
});
it('hands a main-pipeline claim to the rest of the stack, like a reserved name', async () => {
const claims = lookup({ office: { mainPipeline: true } });
for (const [host, subdomains] of [
['office.puter.com', ['office']],
['office.puter.com:4100', ['office']],
['office.x.puter.com', ['x', 'office']],
] as const) {
const args = await runClaimed(
createUserSubdomainNotFound(config, claims),
hostReq(host, [...subdomains]),
);
expect(args, host).toEqual([]);
}
});
it('looks the claim up on every request', async () => {
const claims: Record<string, ResolvedClaim> = {};
const middleware = createUserSubdomainNotFound(config, lookup(claims));
@@ -704,7 +730,7 @@ describe('addClaimedSubdomain', () => {
});
it('rejects subdomains core already handles', () => {
for (const name of ['www', 'api', 'js', 'dav', 'docs', 'onlyoffice']) {
for (const name of ['www', 'api', 'js', 'dav', 'docs']) {
expect(() => addClaimedSubdomain(new Map(), name, claim)).toThrow(
/reserved/,
);
@@ -733,6 +759,26 @@ describe('addClaimedSubdomain', () => {
).toThrow(/invalid checkRoute/);
});
it('records a main-pipeline claim, and refuses one that also carries a gate', () => {
const claims = new Map<string, SubdomainClaim>();
addClaimedSubdomain(claims, 'onlyoffice', { mainPipeline: true });
expect(claims.get('onlyoffice')).toEqual({ mainPipeline: true });
expect(() =>
addClaimedSubdomain(claims, 'onlyoffice', { mainPipeline: true }),
).toThrow(/already claimed/);
for (const gate of [
{ authorize: () => true },
{ checkRoute: () => undefined },
]) {
expect(() =>
addClaimedSubdomain(new Map(), 'portal', {
mainPipeline: true,
...gate,
} as unknown as SubdomainClaim),
).toThrow(/main pipeline with a gate/);
}
});
it('rejects a claim without authorize', () => {
for (const bad of [undefined, {}, { authorize: true }]) {
expect(() =>
@@ -41,8 +41,9 @@ const NATIVE_APP_SUBDOMAINS = [
const NATIVE_APPS_WITH_DIST = new Set(['docs', 'developer']);
/**
* Subdomains that v2 serves itself. Anything NOT in this set that lives on the
* root domain is treated as a user-defined site and rejected with a 404.
* Subdomains that v2 serves itself. A root-domain subdomain neither in this set
* nor claimed by an extension is treated as a user-defined site and rejected
* with a 404.
*
* Kept as a plain Set so `has()` is O(1); order doesn't matter.
*/
@@ -55,8 +56,6 @@ const RESERVED_SUBDOMAINS = new Set<string>([
...NATIVE_APP_SUBDOMAINS,
// App-icon serving subdomain.
'puter-app-icons',
// Extension-owned subdomains.
'onlyoffice',
]);
/**
@@ -69,7 +68,8 @@ export type ClaimAuthorize = (
res: Response,
) => boolean | Promise<boolean>;
export interface SubdomainClaim {
/** A claim answered on its own router, only past `authorize`. */
export interface GatedSubdomainClaim {
authorize: ClaimAuthorize;
/**
* Vets each route declared for the claim as the server builds it; a throw
@@ -78,12 +78,23 @@ export interface SubdomainClaim {
checkRoute?: (options: RouteOptions, label: string) => void;
}
/** A claim as dispatch sees it: its gate, plus the routes served behind it. */
export interface ResolvedClaim {
authorize: ClaimAuthorize;
handler?: RequestHandler;
/** Served like core's own subdomains: CORS, auth and every route. */
export interface MainPipelineSubdomainClaim {
mainPipeline: true;
}
export type SubdomainClaim = GatedSubdomainClaim | MainPipelineSubdomainClaim;
/** A claim as dispatch sees it: a gate and the routes behind it, or neither. */
export type ResolvedClaim =
| { authorize: ClaimAuthorize; handler?: RequestHandler }
| MainPipelineSubdomainClaim;
export const isMainPipelineClaim = (
claim: SubdomainClaim | ResolvedClaim | undefined,
): claim is MainPipelineSubdomainClaim =>
(claim as { mainPipeline?: unknown } | undefined)?.mainPipeline === true;
export type ClaimLookup = (name: string) => ResolvedClaim | undefined;
const SUBDOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -118,6 +129,15 @@ export const addClaimedSubdomain = (
if (claims.has(name)) {
throw new Error(`Subdomain '${name}' is already claimed`);
}
if (isMainPipelineClaim(claim)) {
if ('authorize' in claim || 'checkRoute' in claim) {
throw new Error(
`Subdomain '${name}' claimed for the main pipeline with a gate`,
);
}
claims.set(name, { mainPipeline: true });
return;
}
if (typeof claim?.authorize !== 'function') {
throw new Error(`Subdomain '${name}' claimed without authorize`);
}
@@ -143,7 +163,7 @@ const subdomainNotFound = () =>
* unclaimed-subdomain 404, and errors reach the error handler.
*/
const dispatchClaimed = async (
claim: ResolvedClaim,
claim: Exclude<ResolvedClaim, MainPipelineSubdomainClaim>,
req: Request,
res: Response,
next: NextFunction,
@@ -202,7 +222,8 @@ export const createWwwRedirect = (config: IConfig): RequestHandler => {
* Passes through when:
*
* - No active subdomain (root)
* - Active subdomain is reserved (api, js, native apps, …)
* - Active subdomain is reserved (api, js, native apps, …) or claimed for the
* main pipeline
* - Host is on one of the hosting domains (they may nest under `config.domain`)
* - Host is a local worker host and the local worker server is on
* - Host isn't under `config.domain` (custom domains, other hosts)
@@ -235,6 +256,8 @@ export const createUserSubdomainNotFound = (
req.subdomains?.[req.subdomains.length - 1] ?? ''
).toLowerCase();
if (active === '' || RESERVED_SUBDOMAINS.has(active)) return next();
const claim = lookupClaim(active);
if (isMainPipelineClaim(claim)) return next();
const hostName = (req.headers.host ?? '').toLowerCase().split(':')[0];
if (
@@ -253,7 +276,7 @@ export const createUserSubdomainNotFound = (
const claimed =
req.subdomains?.length === 1 &&
subdomainLabelOf(hostName, domain) === active
? lookupClaim(active)
? claim
: undefined;
if (claimed) {
void dispatchClaimed(claimed, req, res, next);
+7
View File
@@ -161,6 +161,13 @@ describe('extension.claimSubdomain', () => {
expect(extensionStore.claimedSubdomains.get('portal')).toEqual(claim);
});
it('records a main-pipeline claim', () => {
extension.claimSubdomain('onlyoffice', { mainPipeline: true });
expect(extensionStore.claimedSubdomains.get('onlyoffice')).toEqual({
mainPipeline: true,
});
});
it('refuses a second claim, a reserved name, an invalid label and no authorize', () => {
extension.claimSubdomain('portal', claim);
expect(() => extension.claimSubdomain('portal', claim)).toThrow(
+2 -1
View File
@@ -353,7 +353,8 @@ export const extension = {
* Serve `<name>.<config.domain>` in place of the 404 an unclaimed subdomain
* gets. `authorize` runs first, before CORS, body parsing and auth; when it
* returns true, routes declared with `subdomain: name` answer. Anything
* else gets that same 404.
* else gets that same 404. `{ mainPipeline: true }` instead serves the host
* like core's own subdomains, with ordinary routes.
*/
claimSubdomain: (name: string, claim: SubdomainClaim) => {
addClaimedSubdomain(extensionStore.claimedSubdomains, name, claim);
+87
View File
@@ -781,6 +781,93 @@ describe('PuterServer claimed subdomains', () => {
});
});
/**
* A main-pipeline claim is served like a reserved subdomain, with ordinary
* routes.
*/
describe('PuterServer main-pipeline claims', () => {
let server: PuterServer;
let port: number;
const routeCount = { before: 0 };
beforeAll(async () => {
extension.claimSubdomain('mainclaim', { mainPipeline: true });
routeCount.before = extensionStore.routeHandlers.length;
extension.get(
'/hello',
{ subdomain: 'mainclaim' },
(_req: Request, res: Response) => {
res.send('main hello');
},
);
extension.get(
'/signed-in',
{
subdomain: 'mainclaim',
requireAuth: true,
rateLimit: { limit: 5, window: 1000, key: 'user' },
},
(_req: Request, res: Response) => {
res.send('signed in');
},
);
extension.get('/everywhere', { subdomain: '*' }, (_req, res) => {
res.send('everywhere');
});
port = await allocateEphemeralPort();
server = await setupTestServer(
{
port,
domain: 'puter.localhost',
origin: `http://puter.localhost:${port}`,
static_hosting_domain: 'site.puter.localhost',
} as unknown as IConfig,
{ listen: true },
);
});
afterAll(async () => {
extensionStore.claimedSubdomains.delete('mainclaim');
extensionStore.routeHandlers.length = routeCount.before;
await server?.shutdown();
});
it('serves its routes and the main app on the claimed host', async () => {
const host = 'mainclaim.puter.localhost';
expect((await rawRequest(port, '/hello', { host })).body).toBe(
'main hello',
);
const everywhere = await rawRequest(port, '/everywhere', {
host,
origin: 'https://elsewhere.example',
});
const reserved = await rawRequest(port, '/everywhere', {
host: 'api.puter.localhost',
origin: 'https://elsewhere.example',
});
expect(everywhere.body).toBe('everywhere');
for (const res of [everywhere, reserved]) {
expect(res.headers['access-control-allow-origin']).toBe(
'https://elsewhere.example',
);
}
});
it('runs auth for its routes', async () => {
const res = await rawRequest(port, '/signed-in', {
host: 'mainclaim.puter.localhost',
});
expect(res.status).toBe(401);
});
it('keeps its routes off other hosts', async () => {
for (const host of ['api.puter.localhost', 'other.puter.localhost']) {
const res = await rawRequest(port, '/hello', { host });
expect(res.status, host).toBe(404);
}
});
});
describe('PuterServer claimed subdomain route options', () => {
const noop = ((_req: Request, res: Response) =>
res.end()) as unknown as RequestHandler;
+12 -8
View File
@@ -82,6 +82,7 @@ import {
createWwwRedirect,
createUserSubdomainNotFound,
createNativeAppStatic,
isMainPipelineClaim,
} from './core/http/middleware/hostRedirects';
import { createEgressMeteringMiddleware } from './core/http/middleware/egressMetering';
import { createLocalWorkerProxyMiddleware } from './core/http/middleware/localWorkerProxy';
@@ -451,7 +452,7 @@ export class PuterServer {
this.#app.use(
createUserSubdomainNotFound(this.#config, (name) => {
const claim = extensionStore.claimedSubdomains.get(name);
if (!claim) return undefined;
if (!claim || isMainPipelineClaim(claim)) return claim;
return {
authorize: claim.authorize,
handler: this.#claimRouters.get(name),
@@ -1374,26 +1375,29 @@ export class PuterServer {
}
/**
* The claimed subdomain a route is declared for, if any. Claimed hosts
* authenticate through the claim's `authorize`, so options that rely on the
* main pipeline's actor or origin handling fail the boot instead.
* The gated claim a route is declared for, if any. Gated hosts authenticate
* through the claim's `authorize`, so options that rely on the main
* pipeline's actor or origin handling fail the boot instead.
*/
static #claimedSubdomainOf(
opts: RouteOptions,
label: string,
): string | undefined {
const claims = extensionStore.claimedSubdomains;
const gated = (name: string) => {
const claim = extensionStore.claimedSubdomains.get(name);
return isMainPipelineClaim(claim) ? undefined : claim;
};
const labels = [opts.subdomain ?? []].flat();
for (const name of labels) {
const lower = name.toLowerCase();
if (lower !== name && claims.has(lower)) {
if (lower !== name && gated(lower)) {
throw new Error(
`${label}: claimed subdomain '${lower}' must be written in lowercase`,
);
}
}
if (Array.isArray(opts.subdomain)) {
const mixed = opts.subdomain.find((s) => claims.has(s));
const mixed = opts.subdomain.find((s) => gated(s));
if (mixed !== undefined) {
throw new Error(
`${label}: claimed subdomain '${mixed}' must be the route's only subdomain`,
@@ -1402,7 +1406,7 @@ export class PuterServer {
return undefined;
}
if (typeof opts.subdomain !== 'string') return undefined;
const claim = claims.get(opts.subdomain);
const claim = gated(opts.subdomain);
if (!claim) return undefined;
for (const [key, value] of Object.entries(opts)) {
if (value === undefined || value === false) continue;