Skip to content
Merged
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
29 changes: 10 additions & 19 deletions app/parking/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
import type { Metadata } from "next";
import { toTenantParams } from "@/lib/parking";
import TenantPage, { generateMetadata as tenantMetadata } from "../page";
import { permanentRedirect } from "next/navigation";
import { parkingTarget } from "@/lib/parking";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

type SearchParams = Record<string, string | undefined>;

/**
* /parking?name=<domain> — the URL registrar parking pages and forwarding rules
* point at. It renders the tenant page in place rather than redirecting to
* /?dn=<domain>, so the link a registrar already holds keeps working and masked
* forwarding never sees a hop. All other params (?ref, ?brand, ?social_*, …)
* pass straight through to the same renderer as /.
* /parking?name=<name> — where a Moshpit name lands when it has not been
* pointed anywhere yet.
*
* It sends the visitor to the Pit's page for that name rather than rendering a
* parked-domain card here. A 308 (not a 307) because the name's home really is
* `/n/<name>` and always will be: crawlers follow it and index the Pit page,
* instead of holding on to a moshcoding.com URL that only ever bounces.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}): Promise<Metadata> {
const sp = (await searchParams) || {};
return tenantMetadata({ searchParams: Promise.resolve(toTenantParams(sp)) });
}

export default async function Parking({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const sp = (await searchParams) || {};
// No usable name → the tenant renderer falls back to <Landing />, not a 404.
return TenantPage({ searchParams: Promise.resolve(toTenantParams(sp)) });
permanentRedirect(parkingTarget(sp));
}
41 changes: 24 additions & 17 deletions lib/parking.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { safeDomain } from "./config";

export type ParkingParams = Record<string, string | undefined>;

/** The Pit that owns the Moshpit namespace — the registry, not this app. */
export const PIT_BASE_URL = (process.env.PIT_BASE_URL || "https://pit.moshcode.sh").replace(
/\/+$/,
"",
);

/**
* Registrar parking/forwarding links point at /parking?name=<domain>, while the
* tenant renderer at / keys off ?dn=. Map name → dn so both URLs drive one
* implementation instead of two that drift.
* Where /parking?name=<name> belongs.
*
* A Moshpit name that hasn't been pointed anywhere is a name in *our* network,
* so it gets the Pit's own page for it — `/n/<name>`, where you can see who
* holds it and take it if nobody does. Rendering this app's generic
* parked-domain card instead was a dead end: it said "IS COMING" about a name
* that is one click from being yours, and it is not the network we are selling.
*
* Porkbun's param forwarding can glue the visitor's query onto the value
* ("scrambled.eggs?ref=abc"), and safeDomain() strips that back to a bare
* domain — so lift any glued ?ref= into its own param before it's lost. An
* explicit ?ref= already on the URL wins, matching the first-touch rule.
* ("scrambled.eggs?ref=abc"); safeDomain() strips that back to a bare name,
* which is also what keeps this from being an open redirect — the host is
* fixed here and only a validated name is ever appended.
*/
export function toTenantParams(sp: ParkingParams): ParkingParams {
const { name, ...rest } = sp;
const raw = typeof name === "string" && name.trim() ? name : rest.dn;
if (typeof raw !== "string" || !raw.trim()) return rest;

const out: ParkingParams = { ...rest, dn: raw };
if (!out.ref) {
const glued = raw.match(/[?&]ref=([A-Za-z0-9_-]+)/)?.[1];
if (glued) out.ref = glued;
}
return out;
export function parkingTarget(sp: ParkingParams): string {
const name = safeDomain(sp.name ?? sp.dn);
// Nothing usable to look up — the Pit's front door beats a 404 for someone
// who just typed a name at us.
if (!name) return `${PIT_BASE_URL}/pit`;
return `${PIT_BASE_URL}/n/${encodeURIComponent(name)}`;
}
48 changes: 0 additions & 48 deletions tests/parking-params.test.mjs

This file was deleted.

48 changes: 48 additions & 0 deletions tests/parking-target.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import test from "node:test";

import { parkingTarget, PIT_BASE_URL } from "../lib/parking.ts";

test("a Moshpit name goes to the Pit's page for it, not a parked card here", () => {
assert.equal(parkingTarget({ name: "hawaiian.chicken" }), `${PIT_BASE_URL}/n/hawaiian.chicken`);
assert.equal(parkingTarget({ name: "scrambled.eggs" }), `${PIT_BASE_URL}/n/scrambled.eggs`);
});

test("the name is normalized before it becomes a path", () => {
assert.equal(parkingTarget({ name: " Scrambled.EGGS " }), `${PIT_BASE_URL}/n/scrambled.eggs`);
});

test("a Porkbun-glued query is stripped back to the bare name", () => {
// "scrambled.eggs?ref=abc" must not leak into the path.
assert.equal(
parkingTarget({ name: "scrambled.eggs?ref=abc123" }),
`${PIT_BASE_URL}/n/scrambled.eggs`,
);
});

test("?dn= still works, so the older link shape does not break", () => {
assert.equal(parkingTarget({ dn: "moshcode.sh" }), `${PIT_BASE_URL}/n/moshcode.sh`);
});

test("no usable name falls back to the Pit itself rather than a 404", () => {
for (const sp of [{}, { name: " " }, { name: "" }, { name: "notaname" }]) {
assert.equal(parkingTarget(sp), `${PIT_BASE_URL}/pit`, `for ${JSON.stringify(sp)}`);
}
});

test("the target host is fixed, so a hostile name cannot redirect off-site", () => {
// safeDomain() rejects or strips these; the assertion is that nothing which
// survives it can change the host we send people to.
for (const name of [
"evil.com/../../out",
"https://evil.com",
"evil.com#@attacker.test",
"//evil.com",
]) {
const target = parkingTarget({ name });
assert.ok(
target.startsWith(`${PIT_BASE_URL}/`),
`${JSON.stringify(name)} escaped to ${target}`,
);
}
});
Loading