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
2 changes: 2 additions & 0 deletions redirects.caddy
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ redir /docs/self-hosting/railway /actors/self-host/control-plane/{http.request.u
redir /docs/self-hosting/railway/ /actors/self-host/control-plane/{http.request.uri.prefixed_query} 301
redir /docs/self-hosting/tls /actors/self-host/control-plane/{http.request.uri.prefixed_query} 301
redir /docs/self-hosting/tls/ /actors/self-host/control-plane/{http.request.uri.prefixed_query} 301
redir /discord https://discord.gg/rivet-developer-network-822914074136018994{http.request.uri.prefixed_query} 301
redir /discord/ https://discord.gg/rivet-developer-network-822914074136018994{http.request.uri.prefixed_query} 301
redir /docs/integrations/vercel-workflow /actors/integrations/workflow-sdk/{http.request.uri.prefixed_query} 301
redir /docs/integrations/vercel-workflow/ /actors/integrations/workflow-sdk/{http.request.uri.prefixed_query} 301
redir /integrations/vercel-workflow /actors/integrations/workflow-sdk/{http.request.uri.prefixed_query} 301
Expand Down
16 changes: 9 additions & 7 deletions redirects.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
// targets should end in `/` to match the site's canonical trailing-slash form
// and avoid a second redirect hop.
//
// Absolute targets are supported but restricted to a single allowed host, so a
// bad entry can never point traffic at an arbitrary domain. See
// `EXTERNAL_REDIRECT_HOST` below and the matching check in
// `scripts/generate-caddy-redirects.mjs`. Nothing currently uses it: agentOS
// briefly had its own site and is now a vertical here.
// Absolute targets are supported but restricted to explicitly allowed hosts,
// so a bad entry can never point traffic at an arbitrary domain. See
// `EXTERNAL_REDIRECT_HOSTS` below and the matching check in
// `scripts/generate-caddy-redirects.mjs`.
import { readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';


const explicitRedirects = {
// Public community short link. Keep this slashless in authored links, while
// Caddy serves both variants so copied URLs cannot fall through to a 404.
'/discord': 'https://discord.gg/rivet-developer-network-822914074136018994',
// Integrations moved out of the documentation URL hierarchy.
'/docs/integrations/vercel-workflow': '/actors/integrations/workflow-sdk/',
'/integrations/vercel-workflow': '/actors/integrations/workflow-sdk/',
Expand Down Expand Up @@ -249,10 +251,10 @@ function legacyDocsRedirects() {
export const redirects = { ...legacyDocsRedirects(), ...explicitRedirects };


// External host that wildcard and absolute-URL redirect targets are restricted
// External hosts that wildcard and absolute-URL redirect targets are restricted
// to. Used by both the Astro config and the Caddy generator so neither consumer
// can accidentally emit a redirect to an arbitrary host.
export const EXTERNAL_REDIRECT_HOST = 'agentos-sdk.dev';
export const EXTERNAL_REDIRECT_HOSTS = ['agentos-sdk.dev', 'discord.gg'];

// Wildcard (prefix) redirects. Any request under `from` (at any depth) is sent
// to `to`.
Expand Down
3 changes: 2 additions & 1 deletion scripts/check-seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,10 +417,11 @@ for (const file of htmlFiles) {
} catch {
errors.push(`${route}: canonical URL is invalid: ${href}`);
}
if (canonical?.origin !== SITE_ORIGIN) {
if (!redirect && canonical?.origin !== SITE_ORIGIN) {
errors.push(`${route}: canonical must use ${SITE_ORIGIN}`);
}
if (
!redirect &&
canonical &&
ensureDirectorySlash(canonical.pathname) !== canonical.pathname
) {
Expand Down
14 changes: 7 additions & 7 deletions scripts/generate-caddy-redirects.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
// The output file is gitignored and regenerated during the Docker build.
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { EXTERNAL_REDIRECT_HOST, redirects, wildcardRedirects } from '../redirects.mjs';
import { EXTERNAL_REDIRECT_HOSTS, redirects, wildcardRedirects } from '../redirects.mjs';

// Conservative charset for paths. Blocks whitespace and any character that
// could break out of a `redir` argument or inject another Caddy directive.
const SAFE_PATH = /^\/[A-Za-z0-9\-._~/]*$/;

// Absolute external targets are restricted to the single agentOS host with the
// same conservative path charset. This keeps the generator from ever emitting a
// redirect to an arbitrary host while still allowing the agentOS split-out.
const SAFE_EXTERNAL_TARGET = new RegExp(
`^https://${EXTERNAL_REDIRECT_HOST.replace(/\./g, '\\.')}(/[A-Za-z0-9\\-._~/]*)?$`,
// Absolute external targets are restricted to an explicit host allowlist with
// the same conservative path charset. This keeps the generator from ever
// emitting a redirect to an arbitrary host.
const SAFE_EXTERNAL_TARGETS = EXTERNAL_REDIRECT_HOSTS.map(
(host) => new RegExp(`^https://${host.replace(/\./g, '\\.')}(/[A-Za-z0-9\\-._~/]*)?$`),
);

// Expands to `?query` when a query string is present, or an empty string when
Expand All @@ -39,7 +39,7 @@ function assertSafeTarget(to) {
if (typeof to !== 'string') {
throw new Error(`unsafe target in redirect map: ${JSON.stringify(to)}`);
}
if (SAFE_PATH.test(to) || SAFE_EXTERNAL_TARGET.test(to)) {
if (SAFE_PATH.test(to) || SAFE_EXTERNAL_TARGETS.some((pattern) => pattern.test(to))) {
return;
}
throw new Error(`unsafe target in redirect map: ${JSON.stringify(to)}`);
Expand Down
6 changes: 5 additions & 1 deletion src/lib/internalHref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const SITE_ORIGINS = new Set(["https://rivet.dev", "http://rivet.dev"]);
const EDGE_REDIRECT_PATHS = new Set(["/discord"]);

function splitSuffix(value: string): { pathname: string; suffix: string } {
const boundary = value.search(/[?#]/);
Expand All @@ -8,14 +9,17 @@ function splitSuffix(value: string): { pathname: string; suffix: string } {
}

function canonicalizePath(pathname: string): string {
const barePath = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
if (EDGE_REDIRECT_PATHS.has(barePath)) return barePath;
if (!pathname || pathname === "/" || pathname.endsWith("/")) return pathname || "/";
const lastSegment = pathname.slice(pathname.lastIndexOf("/") + 1);
return lastSegment.includes(".") ? pathname : `${pathname}/`;
}

/**
* Returns the canonical trailing-slash form for site-owned directory links.
* Fragments, query strings, files, and external/protocol URLs are preserved.
* Edge-managed short links stay slashless; fragments, query strings, files,
* and external/protocol URLs are preserved.
*/
export function canonicalizeInternalHref(href?: string | null): string {
if (!href) return href ?? "";
Expand Down
Loading