mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-06 14:07:51 +00:00
`extension.claimSubdomain(name, { authorize, checkRoute? })` lets an
extension serve `<name>.<domain>` without a hard-coded reserved name.
- A claimed host is dispatched where the unknown-subdomain 404 used to be,
before CORS, the OPTIONS responder, body parsing, auth and every route
(including `subdomain: '*'` ones).
- `authorize` returning anything but true, or throwing a non-HttpError,
yields exactly the unknown-subdomain 404. A thrown HttpError goes to the
error handler.
- Routes whose `subdomain` is the claimed name (decorated controllers and
extension routes alike) are materialized onto the claim's own router, so
they only run after `authorize` and never on the main app. Their options
are limited to ones that don't rely on the main pipeline's actor; an
optional `checkRoute` lets the claim refuse routes at boot. Case-variant
labels and claimed names mixed into subdomain arrays fail the boot.
- The unknown-subdomain check now compares the port-stripped hostname and
requires a dot boundary, so a host with an explicit port behaves like one
without it. Hosts served by the local worker proxy still pass through.
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
/*
|
|
* Copyright (C) 2024-present Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// Setup for the `shared-worker` vitest project, whose files run with
|
|
// `isolate: false` and so share one module graph per worker. Resets the
|
|
// process-wide state that a fresh worker would otherwise have provided.
|
|
import MockRedis from 'ioredis-mock';
|
|
import { afterAll, beforeAll, vi } from 'vitest';
|
|
import { configContainer } from './exports';
|
|
import { extensionStore } from './extensions';
|
|
// Loaded before the snapshot so anything registered at import time is part
|
|
// of the pristine state.
|
|
import './server';
|
|
|
|
type Container = unknown[] | Map<unknown, unknown> | Record<string, unknown>;
|
|
|
|
// Copies one level deeper than the container: event listener lists are
|
|
// arrays inside `events`, and registering pushes into them.
|
|
const copyContainer = (value: Container): Container => {
|
|
if (Array.isArray(value)) return [...value];
|
|
if (value instanceof Map) return new Map(value);
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([k, v]) => [
|
|
k,
|
|
Array.isArray(v) ? [...v] : v,
|
|
]),
|
|
);
|
|
};
|
|
|
|
// This file is re-evaluated for every test file, so the pristine snapshot
|
|
// lives on globalThis, taken before the first file in the worker runs.
|
|
const SNAPSHOT_KEY = Symbol.for('puter.test.extensionStoreSnapshot');
|
|
const globals = globalThis as {
|
|
[SNAPSHOT_KEY]?: Record<string, Container>;
|
|
};
|
|
globals[SNAPSHOT_KEY] ??= Object.fromEntries(
|
|
Object.entries(extensionStore).map(([k, v]) => [
|
|
k,
|
|
copyContainer(v as Container),
|
|
]),
|
|
);
|
|
|
|
const restoreExtensionStore = () => {
|
|
for (const [key, pristine] of Object.entries(globals[SNAPSHOT_KEY]!)) {
|
|
// The server reads these containers by reference, so refill in place.
|
|
const live = (extensionStore as unknown as Record<string, Container>)[
|
|
key
|
|
];
|
|
const fresh = copyContainer(pristine);
|
|
if (Array.isArray(live)) {
|
|
live.splice(0, live.length, ...(fresh as unknown[]));
|
|
} else if (live instanceof Map) {
|
|
live.clear();
|
|
for (const [k, v] of fresh as Map<unknown, unknown>) live.set(k, v);
|
|
} else {
|
|
for (const k of Object.keys(live)) delete live[k];
|
|
Object.assign(live, fresh);
|
|
}
|
|
}
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
// Every mock client shares the default host/port store, so cached rows
|
|
// from a previous file's server would otherwise survive.
|
|
const redis = new MockRedis();
|
|
await redis.flushall();
|
|
redis.disconnect();
|
|
|
|
for (const key of Object.keys(configContainer)) {
|
|
delete (configContainer as unknown as Record<string, unknown>)[key];
|
|
}
|
|
restoreExtensionStore();
|
|
});
|
|
|
|
afterAll(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
vi.useRealTimers();
|
|
});
|