chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
**/node_modules
.next
__pycache__
*.pyc
.git
shared_frontend/
shared_python/
shared_typescript/
+3
View File
@@ -0,0 +1,3 @@
.next/
tsconfig.tsbuildinfo
node_modules/
+42
View File
@@ -0,0 +1,42 @@
FROM node:20-slim AS builder
ARG COMMIT_SHA=unknown
ARG BRANCH=unknown
WORKDIR /app
# Copy only what shell-dojo + scripts need
COPY showcase/scripts/package.json ./scripts/package.json
COPY showcase/shell-dojo/package.json ./shell-dojo/package.json
# Install deps for both (standalone, no workspace)
RUN cd scripts && npm install --silent && cd ../shell-dojo && npm install --silent
# Copy source
COPY showcase/shared/ ./shared/
COPY showcase/integrations/ ./integrations/
COPY showcase/scripts/ ./scripts/
COPY showcase/shell-dojo/ ./shell-dojo/
# Bake commit info into Next.js build
ENV NEXT_PUBLIC_COMMIT_SHA=${COMMIT_SHA}
ENV NEXT_PUBLIC_BRANCH=${BRANCH}
# Generate registry + content, then build Next.js
RUN cd scripts && node node_modules/tsx/dist/cli.mjs generate-registry.ts \
&& node node_modules/tsx/dist/cli.mjs bundle-demo-content.ts \
&& cd ../shell-dojo && npx next build
FROM node:20-slim AS runner
WORKDIR /app
ARG COMMIT_SHA=unknown
ARG BRANCH=unknown
ENV NODE_ENV=production
ENV PORT=10000
ENV NEXT_PUBLIC_COMMIT_SHA=${COMMIT_SHA}
ENV NEXT_PUBLIC_BRANCH=${BRANCH}
COPY --from=builder /app/shell-dojo/.next ./.next
COPY --from=builder /app/shell-dojo/node_modules ./node_modules
COPY --from=builder /app/shell-dojo/package.json ./
EXPOSE 10000
CMD ["npx", "next", "start", "-p", "10000"]
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d909c834f8551c70656de69513ebad282fef5b9b93c3828175533c3a15c4c49
size 311038
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dfca9c74b55723db0c44faa36db08277ca4ca19e6c21ddd27eab40fd49ab5eb
size 314075
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0a68f69b8890dea464a2f7b909776f08cee1b6a018a72a6f76c14aec32e1878
size 170526
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:afc963b295679c26845855460287a5eff9ae03062223933ce0c4a58442901bfb
size 297633
+21
View File
@@ -0,0 +1,21 @@
import type { NextConfig } from "next";
import path from "path";
import { fileURLToPath } from "url";
import { localBackendsEnv } from "./src/lib/local-backends-env";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LOCAL_PORTS_PATH = path.resolve(
__dirname,
"..",
"shared",
"local-ports.json",
);
const nextConfig: NextConfig = {
env: {
NEXT_PUBLIC_LOCAL_BACKENDS: localBackendsEnv(LOCAL_PORTS_PATH),
},
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@copilotkit/showcase-shell-dojo",
"version": "0.1.0",
"private": true,
"scripts": {
"generate": "tsx ../scripts/generate-registry.ts && tsx ../scripts/bundle-demo-content.ts",
"predev": "tsx ../scripts/generate-registry.ts && tsx ../scripts/bundle-demo-content.ts",
"dev": "next dev --turbopack --port 3001",
"build": "tsx ../scripts/generate-registry.ts && tsx ../scripts/bundle-demo-content.ts && next build",
"start": "next start"
},
"dependencies": {
"highlight.js": "^11.11.1",
"next": "^15.5.15",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-syntax-highlighter": "^15.6.0",
"recharts": "^2.15.0",
"zod": "^3.24.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-syntax-highlighter": "^15.5.0",
"postcss": "^8.5.0",
"tailwindcss": "^4.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+47
View File
@@ -0,0 +1,47 @@
@import "tailwindcss";
@import "highlight.js/styles/github.css";
:root {
/* CopilotCloud palette — exact from dojo */
--surface-main: #dedee9;
--surface-container: #ffffff;
--border-default: #ffffff;
--border-container: #dbdbe5;
--text-primary: #010507;
--text-secondary: #57575b;
--text-disabled: #838389;
--text-invert: #ffffff;
--sidebar-width: 296px;
--font-mono: "Spline Sans Mono", ui-monospace, SFMono-Regular, monospace;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--surface-main);
color: var(--text-primary);
font-family:
"Plus Jakarta Sans",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
-webkit-font-smoothing: antialiased;
}
/* Scrollbar styling for sidebar */
.sidebar-scroll::-webkit-scrollbar {
width: 4px;
}
.sidebar-scroll::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-scroll::-webkit-scrollbar-thumb {
background: var(--border-container);
border-radius: 4px;
}
+71
View File
@@ -0,0 +1,71 @@
import "./globals.css";
import type { Metadata } from "next";
import { getRuntimeConfig } from "@/lib/runtime-config";
export const metadata: Metadata = {
title: "CopilotKit Interactive Dojo",
description: "Interactive showcase of CopilotKit integrations",
};
/**
* Serialize the runtime config for inline injection. We must
* JSON.stringify-then-escape because the value lands inside a
* `<script>...</script>` tag, where three substrings would otherwise
* break out of (or corrupt) the parser:
*
* - `<` — guards against the `</script>` breakout (XSS).
* `JSON.stringify` does NOT escape `<` by default, so a URL
* containing `</script>` (e.g. a hostile env value) would
* terminate the inline script and inject HTML. Escape every
* `<` to `<` so the substring `</script>` can never appear.
* - `
` (LINE SEPARATOR) and `
` (PARAGRAPH SEPARATOR) —
* legal inside JSON strings, but a syntax error inside a JS
* string literal in older engines / when the page is parsed as
* `text/javascript`. Escape both.
*
* Canonical OWASP-recommended escape for inline JSON in HTML.
*/
function serializeRuntimeConfig(cfg: unknown): string {
return JSON.stringify(cfg)
.replace(/</g, "\\u003c")
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
}
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Server-side: read live env at request time. `unstable_noStore()`
// inside getRuntimeConfig opts this segment out of the static cache
// so the inline <script> below always reflects the current Railway
// env vars. The serialized config carries backendHostPattern, which
// the client reader uses to derive each integration's preview backend
// URL at request time (see lib/runtime-config.ts).
const runtimeConfig = getRuntimeConfig();
const injection = `window.__SHOWCASE_CONFIG__=${serializeRuntimeConfig(runtimeConfig)};`;
return (
<html lang="en">
<head>
{/* First child of <head> — MUST execute before every other
head-level script so client code can read
window.__SHOWCASE_CONFIG__ during module init. Keep this
ahead of the fonts <link> and any future next/script
beforeInteractive blocks. */}
<script
id="__showcase_config__"
dangerouslySetInnerHTML={{ __html: injection }}
/>
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Spline+Sans+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
</head>
<body>{children}</body>
</html>
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,141 @@
"use client";
import { useMemo } from "react";
import hljs from "highlight.js/lib/core";
import typescript from "highlight.js/lib/languages/typescript";
import python from "highlight.js/lib/languages/python";
import csharp from "highlight.js/lib/languages/csharp";
import json from "highlight.js/lib/languages/json";
import yaml from "highlight.js/lib/languages/yaml";
import css from "highlight.js/lib/languages/css";
import markdown from "highlight.js/lib/languages/markdown";
hljs.registerLanguage("typescript", typescript);
hljs.registerLanguage("javascript", typescript);
hljs.registerLanguage("python", python);
hljs.registerLanguage("csharp", csharp);
hljs.registerLanguage("json", json);
hljs.registerLanguage("yaml", yaml);
hljs.registerLanguage("css", css);
hljs.registerLanguage("markdown", markdown);
const LANGUAGES_SUPPORTED = new Set([
"typescript",
"javascript",
"python",
"csharp",
"json",
"yaml",
"css",
"markdown",
]);
interface CodeBlockProps {
code: string;
language: string;
/**
* 1-based line numbers that should render with a region-highlight
* background. Empty / undefined means no highlighting.
*/
highlightedLines?: ReadonlySet<number>;
}
const SCROLL_CONTAINER_STYLE = {
display: "flex",
flex: 1,
overflow: "auto",
background: "#ffffff",
fontFamily: "'Spline Sans Mono', 'SF Mono', Menlo, Consolas, monospace",
fontSize: 13,
lineHeight: 1.5,
} as const;
const LINES_WRAPPER_STYLE = {
display: "inline-block",
minWidth: "100%",
padding: "16px 0",
} as const;
const LINE_ROW_BASE_STYLE = {
display: "flex",
} as const;
const LINE_NUMBER_STYLE = {
flexShrink: 0,
textAlign: "right",
userSelect: "none",
color: "#838389",
padding: "0 12px 0 16px",
whiteSpace: "pre",
} as const;
const LINE_CODE_STYLE = {
flex: 1,
whiteSpace: "pre",
paddingRight: 16,
} as const;
const HIGHLIGHTED_BG = "rgba(250, 204, 21, 0.22)";
export function CodeBlock({
code,
language,
highlightedLines,
}: CodeBlockProps) {
// Highlight the whole file once, then split into per-line HTML strings.
// Splitting after highlighting means single-line constructs render with
// correct token classes; multi-line block comments / template literals
// can lose their wrapping span at the line break, but that's a minor
// cosmetic glitch in exchange for cheap per-line backgrounds.
const lineHtml = useMemo(() => {
const lang = language?.toLowerCase();
const raw = code.endsWith("\n") ? code.slice(0, -1) : code;
if (!lang || !LANGUAGES_SUPPORTED.has(lang)) {
return raw.split("\n").map(escapeHtml);
}
try {
return hljs.highlight(raw, { language: lang }).value.split("\n");
} catch {
return raw.split("\n").map(escapeHtml);
}
}, [code, language]);
const pad = String(lineHtml.length).length;
const highlightedRowStyle = useMemo(
() => ({ ...LINE_ROW_BASE_STYLE, background: HIGHLIGHTED_BG }),
[],
);
return (
<div style={SCROLL_CONTAINER_STYLE}>
<div style={LINES_WRAPPER_STYLE}>
{lineHtml.map((html, i) => {
const lineNum = i + 1;
const isHighlighted = highlightedLines?.has(lineNum) ?? false;
return (
<div
key={i}
style={isHighlighted ? highlightedRowStyle : LINE_ROW_BASE_STYLE}
>
<div style={LINE_NUMBER_STYLE}>
{String(lineNum).padStart(pad, " ")}
</div>
<div
style={LINE_CODE_STYLE}
// hljs returns sanitized HTML and the highlighted source is
// not user input — this content is bundled at build time
// from files in `showcase/integrations/`.
dangerouslySetInnerHTML={{ __html: html || " " }}
/>
</div>
);
})}
</div>
</div>
);
}
function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
+484
View File
@@ -0,0 +1,484 @@
// Runtime derivation of integration backend URLs.
//
// The registry's `backend_url` is synthesized at Docker BUILD time by
// scripts/generate-registry.ts, which bakes the production hostnames
// into every image — so a staging shell iframed prod integrations.
// These helpers derive the backend host at REQUEST time from the
// `backendHostPattern` carried in RuntimeConfig (env var
// SHOWCASE_BACKEND_HOST_PATTERN, default = the prod pattern), keeping
// registry.json as the source for non-URL metadata only.
//
// Pattern semantics are IDENTICAL to generate-registry.ts: the pattern
// is a bare host with `{slug}` as the only placeholder, and `https://`
// is prepended. Keep the two in sync — they consume the same env var.
//
// This module is import-safe from client components, server components,
// and middleware (pure functions, no next/* imports).
// Matches an explicit URL scheme prefix (e.g. `https://`, `http://`).
// Exported as the single source of truth — runtime-config.ts shares it
// (this module is import-safe everywhere, so the dependency is free).
export const SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
// Default backend host pattern — reproduces today's baked prod values
// exactly, so a deploy with the env var unset (i.e. current prod)
// behaves byte-identically. Lives HERE (not runtime-config.ts, which
// re-exports it) because normalizeBackendHostPattern falls back to it
// for degenerate values and runtime-config already imports this module
// — defining it there would create an import cycle.
export const DEFAULT_BACKEND_HOST_PATTERN =
"showcase-{slug}-production.up.railway.app";
// Warn once per distinct (pattern, issue) — config is re-read every
// request, and per-request warn spam would drown real signal.
const patternWarnings = new Set<string>();
function warnPatternOnce(key: string, message: string): void {
if (patternWarnings.has(key)) return;
patternWarnings.add(key);
// eslint-disable-next-line no-console
console.warn(`[backend-url] SHOWCASE_BACKEND_HOST_PATTERN ${message}`);
}
// FATAL once per distinct degenerate pattern value — parity with the
// FATAL-CONFIG once-guards in runtime-config.ts (readUrl/readDocsHost).
const patternFatals = new Set<string>();
// Same dev-vs-prod branch as validateBaseUrl/readDocsHost
// (runtime-config.ts): in production this is a FATAL-CONFIG error with
// Railway guidance; in dev it degrades to a warn — Railway guidance is
// useless on a laptop, and the dev contract is frictionless iteration.
// NODE_ENV is read at CALL time (this module is import-safe everywhere;
// Next statically inlines the literal read in client bundles).
function fatalPatternOnce(key: string, message: string): void {
if (patternFatals.has(key)) return;
patternFatals.add(key);
if (process.env.NODE_ENV === "production") {
// eslint-disable-next-line no-console
console.error(
`[backend-url] FATAL-CONFIG: SHOWCASE_BACKEND_HOST_PATTERN ${message} ` +
`Fix the SHOWCASE_BACKEND_HOST_PATTERN env var on the Railway service.`,
);
} else {
// eslint-disable-next-line no-console
console.warn(`[backend-url] SHOWCASE_BACKEND_HOST_PATTERN ${message}`);
}
}
/**
* Can the normalized pattern actually form a backend URL? Probe by
* substituting a registry-shaped slug and parsing the consumer's exact
* composition (`https://` + pattern). Catches the degenerate classes
* the per-issue normalizations can't fix: empty results ("https://" or
* "/" normalize to ""), internal whitespace, and anything else
* `new URL` rejects or that parses without a real host.
*
* Returns `undefined` when usable, otherwise a reason discriminant so
* the FATAL can be honest: a probe whose HOST is literally
* "http"/"https" (e.g. `https:/host` — one slash short, so SCHEME_RE
* never strips it) DOES parse — calling it "unparseable" would hide
* the actual problem (a stray scheme fragment in host position).
*/
function patternUsabilityProblem(
normalized: string,
): "unparseable" | "stray-scheme-fragment" | undefined {
if (normalized.length === 0) return "unparseable";
try {
const probe = new URL(
`https://${normalized.replaceAll("{slug}", "probe")}`,
);
if (probe.hostname.length === 0) return "unparseable";
if (/^https?$/i.test(probe.hostname)) return "stray-scheme-fragment";
return undefined;
} catch {
return "unparseable";
}
}
/**
* Does the pattern carry a component no backend base URL may have?
* Detected on the LITERAL delimiter characters (see inline comments —
* the WHATWG getters return "" for present-but-empty components, so a
* probe-based check leaks bare `?`/`#`/`@`).
* Returns a human-readable component name for the FATAL log, or
* undefined when the pattern is clean. Three rejected classes (same
* gates validateBaseUrl has in runtime-config.ts):
*
* - userinfo credentials: a credentialed pattern yields iframe srcs
* that Chromium silently blocks — the integration pane just never
* loads, with zero signal;
* - query / fragment: consumers concatenate demo routes onto the
* composed URL, so `https://host?x=1` + `/route` yields
* `https://host?x=1/route` — every backend URL ships corrupted.
*/
function patternForbiddenComponent(normalized: string): string | undefined {
// Query/fragment are detected on the LITERAL characters, not the
// probe getters: WHATWG getters return "" for a present-but-EMPTY
// component (`https://host?` has search === ""), so a bare trailing
// `?`/`#` would slip a getter check while the raw character still
// ships in the pattern — and route concatenation then swallows every
// demo route into the query/fragment. The literal check is strictly
// stronger than the getter check (a non-empty search/hash implies the
// literal character), so the getters aren't consulted at all. Which
// component a literal introduces follows URL precedence: `#` starts
// the fragment; `?` starts a query only when no `#` precedes it.
const hashAt = normalized.indexOf("#");
const queryAt = normalized.indexOf("?");
if (queryAt !== -1 && (hashAt === -1 || queryAt < hashAt)) {
return "a query component";
}
if (hashAt !== -1) return "a fragment component";
// Userinfo is a literal check too: `https://@host` and
// `https://:@host` parse with username === "" AND password === ""
// (present-but-EMPTY userinfo), so the getter check let the raw
// `@`-bearing string ship. Any `@` in the AUTHORITY (the part before
// the first `/` — query/fragment are already rejected above) is a
// userinfo delimiter; it is never valid inside a hostname.
const slashAt = normalized.indexOf("/");
const authority = slashAt === -1 ? normalized : normalized.slice(0, slashAt);
if (authority.includes("@")) return "userinfo credentials";
return undefined;
}
/**
* Normalize a backend host pattern read from the environment. The
* pattern contract (same as scripts/generate-registry.ts) is a bare
* host with `{slug}` as the only placeholder — but env misconfigs are
* easy and were previously silent:
*
* - a scheme-bearing value would yield `https://https://…` (consumer
* prepends the scheme) → strip it and warn;
* - a trailing slash would yield `host//route` on concat → trim and warn;
* - a missing `{slug}` placeholder silently sends EVERY integration to
* the same host → warn (can't fix it for the operator).
*
* Warnings fire once per distinct value, not per request.
*/
export function normalizeBackendHostPattern(pattern: string): string {
let normalized = pattern;
if (/\s/.test(normalized)) {
// Whitespace was the ONE misconfig class with zero warning — a
// pasted ` host` yields an iframe src like `https:// host`. Trim
// the ends (fixable) and strip internal tab/CR/LF (the WHATWG URL
// parser deletes \t\r\n pre-parse, so a tab-bearing pattern would
// pass the usability gate while the RAW control character shipped
// in every iframe src — stripping matches what the parser does
// anyway). Internal SPACES split by position: a host-position
// space makes the pattern unparseable, so the usability gate below
// falls back to the DEFAULT (it never ships); a path-position
// space parses and genuinely ships broken.
warnPatternOnce(
`whitespace:${pattern}`,
`"${pattern}" contains whitespace — trimming the ends and stripping internal tab/CR/LF (the URL parser ignores them anyway). An internal space in the host cannot form a usable pattern (falls back to the default); a space in a path segment ships in every backend URL.`,
);
normalized = normalized.replace(/[\t\r\n]/g, "").trim();
}
// Loop the strip to convergence: a single pass left "https://https://
// host" with a scheme that the consumer then double-prepends. Collect
// every stripped scheme first so the once-guarded warn can name them
// ALL — warning inside the loop swallowed every name after the first.
const strippedSchemes: string[] = [];
for (
let scheme = SCHEME_RE.exec(normalized);
scheme;
scheme = SCHEME_RE.exec(normalized)
) {
strippedSchemes.push(scheme[0]);
normalized = normalized.slice(scheme[0].length);
}
if (strippedSchemes.length > 0) {
warnPatternOnce(
`scheme:${pattern}`,
`"${pattern}" includes a scheme — the consumer prepends https://; stripping ${strippedSchemes
.map((s) => `"${s}"`)
.join(", ")}.`,
);
}
if (/\/+$/.test(normalized)) {
warnPatternOnce(
`trailing-slash:${pattern}`,
`"${pattern}" has a trailing slash — route concatenation would yield "//"; trimming.`,
);
normalized = normalized.replace(/\/+$/, "");
}
// Usability gate AFTER the fixable normalizations, BEFORE the
// advisory warns ({slug}/path) — a degenerate value ("https://", "/",
// whitespace) normalizes to "" or an unparseable host, and previously
// flowed out with NO fallback: server-side iframe srcs became
// "https://", and the injected "" failed the client reader's
// REQUIRED_CONFIG_FIELDS check, crashing every client component with
// a message blaming the layout injection instead of this env var.
//
// DESIGN DECISION — degenerate/forbidden patterns fail OPEN to
// DEFAULT_BACKEND_HOST_PATTERN, which is the PROD host pattern. On a
// staging deploy, a mistyped SHOWCASE_BACKEND_HOST_PATTERN therefore
// silently (FATAL log aside) iframes PROD integrations — the exact
// staging→prod leakage this module exists to prevent. The
// alternative (fail CLOSED: ship "" and break every integration
// pane) was rejected because a hard outage is worse than serving
// prod content with a FATAL-CONFIG log; revisit if staging/prod
// divergence ever becomes load-bearing for correctness.
const forbidden = patternForbiddenComponent(normalized);
if (forbidden !== undefined) {
fatalPatternOnce(
`forbidden:${forbidden}:${pattern}`,
`${JSON.stringify(pattern)} carries ${forbidden} — userinfo makes ` +
`Chromium silently block every iframe src formed from it, and a ` +
`query/fragment corrupts every backend URL when consumers ` +
`concatenate demo routes; falling back to the default pattern ` +
`${DEFAULT_BACKEND_HOST_PATTERN}.`,
);
return DEFAULT_BACKEND_HOST_PATTERN;
}
// Fails OPEN to the prod pattern too — see the DESIGN DECISION note
// above the forbidden-component gate.
const usabilityProblem = patternUsabilityProblem(normalized);
if (usabilityProblem !== undefined) {
fatalPatternOnce(
`degenerate:${pattern}`,
usabilityProblem === "stray-scheme-fragment"
? `${JSON.stringify(pattern)} normalizes to ` +
`${JSON.stringify(normalized)}, whose host is literally ` +
`"http"/"https" — it looks like a stray scheme fragment, not ` +
`a backend host; falling back to the default pattern ` +
`${DEFAULT_BACKEND_HOST_PATTERN}.`
: `${JSON.stringify(pattern)} normalizes to ` +
`${JSON.stringify(normalized)}, which cannot form a parseable ` +
`backend URL; falling back to the default pattern ` +
`${DEFAULT_BACKEND_HOST_PATTERN}.`,
);
return DEFAULT_BACKEND_HOST_PATTERN;
}
// Canonicalize the authority for parity with the override path,
// which returns the parsed-normalized form (lowercase host, default
// port elided) — the pattern path ships this string RAW into iframe
// srcs, leaking uppercase hosts and an explicit :443. Hosts are
// case-insensitive and the consumer always prepends https://, so
// lowercasing the authority and stripping a trailing :443 (the https
// default — :80 is a REAL port under https and must survive) matches
// what the URL parser does to the composed URL anyway. The path part
// (from the first "/") is case-SENSITIVE and stays untouched; the
// lowercase {slug} placeholder survives lowercasing verbatim.
const pathAt = normalized.indexOf("/");
const authority = pathAt === -1 ? normalized : normalized.slice(0, pathAt);
const pathPart = pathAt === -1 ? "" : normalized.slice(pathAt);
normalized = authority.toLowerCase().replace(/:443$/, "") + pathPart;
if (!normalized.includes("{slug}")) {
warnPatternOnce(
`no-slug:${pattern}`,
`"${pattern}" lacks the {slug} placeholder — EVERY integration will resolve to the same backend host.`,
);
}
if (normalized.includes("/")) {
// The documented contract is a BARE host — an internal path segment
// (`host.app/base/{slug}`) violates it silently: the consumer
// prepends https:// and concatenates routes, so the base path lands
// in every backend URL. Can't fix it for the operator (the intent
// is ambiguous) — flag it.
warnPatternOnce(
`path:${pattern}`,
`"${pattern}" contains a path segment — the contract is a bare host; the path will be embedded in every backend URL.`,
);
}
return normalized;
}
// Registry slug contract — see scripts/generate-registry.ts manifests.
const SLUG_RE = /^[a-z0-9-]+$/;
/** Substitute `{slug}` into the host pattern and prepend `https://`. */
export function backendUrlFromPattern(pattern: string, slug: string): string {
// Charset assert at the choke point: every backend URL flows through
// here and the slug lands in the HOST of an iframe src — a slug
// containing "." or "/" is host/path injection. All registry slugs
// match [a-z0-9-]+; anything else is a contract violation upstream
// (call sites resolve slugs from the registry), not data.
if (!SLUG_RE.test(slug)) {
throw new Error(
`[backend-url] invalid integration slug ${JSON.stringify(slug)}` +
`slugs must match ${String(SLUG_RE)} (host-injection guard).`,
);
}
// Function replacer: a plain string replacement is subject to `$`
// substitution patterns ("$&", "$'", ...), which would corrupt the
// host for any slug containing `$` (defense in depth behind SLUG_RE).
return `https://${pattern.replaceAll("{slug}", () => slug)}`;
}
// Warn once per distinct (raw value, issue) — resolveBackendUrl runs on
// every render, so per-call warns would spam exactly like the pattern
// warnings the patternWarnings set above exists to prevent.
const localBackendsWarnings = new Set<string>();
function warnLocalBackendsOnce(key: string, message: string): void {
if (localBackendsWarnings.has(key)) return;
localBackendsWarnings.add(key);
// eslint-disable-next-line no-console
console.warn(`[backend-url] NEXT_PUBLIC_LOCAL_BACKENDS ${message}`);
}
// Shared frozen empty map for the unset path — a fresh mutable {} per
// call would dodge the freeze guarantee below.
const NO_LOCAL_BACKENDS: Record<string, string> = Object.freeze({});
// Memoized on the raw string: the env value is baked at build time and
// effectively constant, so re-running JSON.parse on every render is
// pure waste.
let lastLocalBackendsRaw: string | undefined;
let lastLocalBackends: Record<string, string> = NO_LOCAL_BACKENDS;
/**
* Parse the NEXT_PUBLIC_LOCAL_BACKENDS map (baked at build from
* shared/local-ports.json — local-dev only, empty in deployed images).
* Tolerant of unset/empty/corrupt values: local dev convenience must
* never break rendering. The parse is memoized on the raw string and
* warnings fire once per distinct (value, issue), not per call.
*
* The returned object is FROZEN: the memo is shared across every
* caller, so a consumer mutating its "own" map would change the
* local-backend overrides process-wide.
*/
export function parseLocalBackends(
raw: string | undefined,
): Record<string, string> {
if (!raw) return NO_LOCAL_BACKENDS;
if (raw !== lastLocalBackendsRaw) {
// Compute FIRST, then commit key and value together: committing
// the key before the value meant a throw mid-compute (console.warn
// is a foreign call — a logging shim CAN throw) left the memo
// poisoned, and the next call with the same raw returned the
// PREVIOUS raw's value.
const backends = Object.freeze(parseLocalBackendsUncached(raw));
lastLocalBackendsRaw = raw;
lastLocalBackends = backends;
}
return lastLocalBackends;
}
function parseLocalBackendsUncached(raw: string): Record<string, string> {
try {
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
// Validate values instead of an unchecked `as Record<string,
// string>` flow-through — a non-string value would otherwise
// surface much later as a garbage iframe src.
//
// Null-prototype accumulator: on a plain `{}`, assigning a
// "__proto__" key hits the Object.prototype setter and is a
// silent no-op — the entry would vanish with no warning (every
// other rejected entry warns). With no prototype it lands as an
// ordinary own data property.
const backends: Record<string, string> = Object.create(null);
for (const [slug, url] of Object.entries(parsed)) {
if (typeof url === "string") {
backends[slug] = url;
} else {
warnLocalBackendsOnce(
`non-string:${slug}:${raw}`,
`value for "${slug}" is not a string — skipping it.`,
);
}
}
return backends;
}
warnLocalBackendsOnce(
`non-object:${raw}`,
"is not a JSON object — ignoring it.",
);
} catch {
// Treat unparseable as unset (local-dev convenience must never
// break rendering) — but say so, instead of silently eating it.
warnLocalBackendsOnce(
`invalid-json:${raw}`,
"is not valid JSON — ignoring it.",
);
}
return {};
}
/**
* Resolve an integration's backend base URL: a local-dev override from
* NEXT_PUBLIC_LOCAL_BACKENDS wins (preserving the pre-existing
* `SHOWCASE_LOCAL=1` behavior), otherwise derive from the runtime host
* pattern.
*/
export function resolveBackendUrl(slug: string, pattern: string): string {
// ASSUMPTION: the server must never SET NEXT_PUBLIC_LOCAL_BACKENDS at
// runtime post-build — the client bundle bakes the build-time value,
// so a live server-side value would silently diverge from what client
// components resolve.
const local = parseLocalBackends(process.env.NEXT_PUBLIC_LOCAL_BACKENDS);
const rawOverride = local[slug];
if (rawOverride !== undefined) {
// Trim before validating — the same paste-artifact tolerance the
// pattern path applies (normalizeBackendHostPattern trims the
// ends): a leading-space override otherwise fails the SCHEME_RE
// anchor even though the URL parser accepts the value.
const override = rawOverride.trim();
// Length-aware: an empty/whitespace-only override (e.g.
// `{"mastra": ""}`) is not a usable URL — `??` would accept it and
// yield an empty base. The emptiness check lives INSIDE the
// override block so it warns like every other bad override:
// skipping it in the outer condition ignored the dead override
// with zero signal.
if (override.length === 0) {
warnLocalBackendsOnce(
`bad-override:${slug}:empty`,
`override for "${slug}" is empty (or whitespace-only) — ignoring it.`,
);
return backendUrlFromPattern(pattern, slug);
}
// The override lands verbatim in an iframe src — require a
// scheme-bearing, parseable URL (`localhost:4111` without a scheme
// parses as scheme "localhost:"!) whose protocol is http(s):
// `javascript://...` and `ftp://...` are scheme-bearing AND
// parseable, but have no business in an iframe src. Userinfo,
// query, and fragment components are rejected too (same gates the
// pattern path has): Chromium silently blocks credentialed iframe
// srcs, and a query/fragment corrupts every composed URL when
// consumers concatenate demo routes. Warn and fall back otherwise.
const parsed = parseUrl(override);
if (
parsed !== undefined &&
SCHEME_RE.test(override) &&
/^https?:$/i.test(parsed.protocol) &&
parsed.username === "" &&
parsed.password === "" &&
parsed.search === "" &&
parsed.hash === ""
) {
// Return the parsed-normalized form (origin + path), not the raw
// string: the parse already happened, and the raw form leaks
// un-normalized values (uppercase hosts, explicit default ports)
// into iframe srcs. The trailing-slash trim matches the
// normalization the pattern path guarantees
// (normalizeBackendHostPattern) — an override skipping it yields
// `host//route` when consumers concatenate demo routes. Query,
// fragment, and userinfo are empty here (gated above), so
// origin + pathname IS the whole URL.
return (parsed.origin + parsed.pathname).replace(/\/+$/, "");
}
// Name the actual requirements instead of "not parseable":
// `http:localhost:4111` IS parseable (special schemes tolerate
// missing slashes), but it fails the explicit `scheme://`
// requirement — a "not parseable" claim sends the developer
// chasing a parse problem that doesn't exist.
warnLocalBackendsOnce(
`bad-override:${slug}:${override}`,
`override for "${slug}" (${JSON.stringify(override)}) must be an ` +
`http(s) URL with an explicit "scheme://" and no userinfo, query, ` +
`or fragment — ignoring it.`,
);
}
return backendUrlFromPattern(pattern, slug);
}
function parseUrl(value: string): URL | undefined {
// URL.canParse needs Node 18.17+/modern browsers — try/catch keeps
// this safe on every runtime the shell targets.
try {
return new URL(value);
} catch {
return undefined;
}
}
@@ -0,0 +1,69 @@
import fs from "fs";
/**
* Build-time helper for local Dojo runs.
*
* When SHOWCASE_LOCAL=1, Next bakes a slug -> localhost URL map from
* shared/local-ports.json into NEXT_PUBLIC_LOCAL_BACKENDS. Deployed builds leave
* this empty and keep using registry backend_url values.
*/
export function localBackendsEnv(portsPath: string): string {
const rawLocal = process.env.SHOWCASE_LOCAL;
const showcaseLocal = rawLocal === undefined ? "" : rawLocal.trim();
if (showcaseLocal !== "1") {
if (showcaseLocal !== "") {
console.warn(
`[next.config] SHOWCASE_LOCAL is set to ${JSON.stringify(rawLocal)} ` +
`but only "1" enables local backend overrides — treating it as off.`,
);
}
return "";
}
let rawText: string;
try {
rawText = fs.readFileSync(portsPath, "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
console.warn(
`[next.config] SHOWCASE_LOCAL=1 but ${portsPath} does not exist — ` +
`no local backend overrides will be baked.`,
);
return "";
}
throw new Error(`${portsPath} could not be read: ${String(err)}`, {
cause: err,
});
}
let parsed: unknown;
try {
parsed = JSON.parse(rawText);
} catch (err) {
throw new Error(`${portsPath} is not valid JSON: ${String(err)}`, {
cause: err,
});
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${portsPath} must be a JSON object mapping slug -> port.`);
}
const map: Record<string, string> = Object.create(null);
for (const [slug, port] of Object.entries(parsed)) {
if (
typeof port !== "number" ||
!Number.isInteger(port) ||
port <= 0 ||
port > 65535
) {
throw new Error(
`${portsPath}: port for "${slug}" is not a valid TCP port; got ` +
`${JSON.stringify(port)}.`,
);
}
map[slug] = `http://localhost:${port}`;
}
return JSON.stringify(map);
}
+87
View File
@@ -0,0 +1,87 @@
import registryData from "@/data/registry.json";
export interface Feature {
id: string;
name: string;
category: string;
description: string;
}
export interface FeatureCategory {
id: string;
name: string;
}
export interface Demo {
id: string;
name: string;
description: string;
tags: string[];
route: string;
animated_preview_url?: string | null;
}
export interface Integration {
name: string;
slug: string;
category: string;
language: string;
logo?: string;
description: string;
partner_docs: string | null;
repo: string;
copilotkit_version: string;
backend_url: string;
deployed: boolean;
sort_order?: number;
features: string[];
demos: Demo[];
}
export interface Registry {
feature_registry: {
version: string;
categories: FeatureCategory[];
features: Feature[];
};
integrations: Integration[];
}
const registry = registryData as Registry;
// Hide registry entries with no dojo route. Kept in the registry so
// harness/parity/dashboard still see them.
const HIDDEN_DOJO_FEATURE_IDS = new Set<string>(["cli-start"]);
function withVisibleDemos(integration: Integration): Integration {
const visible = integration.demos.filter(
(d) => !HIDDEN_DOJO_FEATURE_IDS.has(d.id),
);
if (visible.length === integration.demos.length) return integration;
return { ...integration, demos: visible };
}
export function getRegistry(): Registry {
return registry;
}
export function getIntegrations(): Integration[] {
return registry.integrations.map(withVisibleDemos);
}
export function getIntegration(slug: string): Integration | undefined {
const found = registry.integrations.find((i) => i.slug === slug);
return found ? withVisibleDemos(found) : undefined;
}
export function getFeatures(): Feature[] {
return registry.feature_registry.features;
}
export function getFeature(id: string): Feature | undefined {
return registry.feature_registry.features.find((f) => f.id === id);
}
export function getFeatureCategories(): FeatureCategory[] {
return registry.feature_registry.categories;
}
@@ -0,0 +1,80 @@
// Client-side runtime config reader. Reads from
// window.__SHOWCASE_CONFIG__ which the root layout injects via an
// inline <script> tag BEFORE React hydrates (see app/layout.tsx). This
// is the ONLY public API for these URLs in client code — never read
// process.env.NEXT_PUBLIC_* directly (an ESLint rule enforces this).
import type { RuntimeConfig } from "./runtime-config";
export type { RuntimeConfig };
declare global {
interface Window {
__SHOWCASE_CONFIG__?: RuntimeConfig;
}
}
/**
* Sentinel returned during SSR when `window` is unavailable. "use client"
* components in the Next.js App Router are server-side rendered on the
* initial request (that's how the HTML is streamed before hydration),
* which means their function bodies execute on the server too. We can't
* throw here without breaking SSR — instead we return a placeholder.
*
* The {slug} placeholder is preserved so an SSR-phase substitution still
* yields a parseable, RFC-2606-unresolvable host (`.invalid`). No iframe
* ever renders from this: the page gates its preview iframe on a
* client-mounted flag, so backendHostPattern is only ever read after
* hydration when window.__SHOWCASE_CONFIG__ carries the real value.
* Server components that need the live env values MUST import
* getRuntimeConfig from runtime-config.ts (the server variant).
*/
const SSR_PLACEHOLDER: Readonly<RuntimeConfig> = Object.freeze({
backendHostPattern: "showcase-{slug}.ssr-placeholder.invalid",
});
/**
* Returns the runtime config injected by the root server layout.
*
* During SSR (no window) returns a sentinel placeholder; the inline
* <script> runs before hydration, so every client render — including
* the hydration render — sees the real values. If the inline <script>
* never ran (a route bypassed the layout) or injected an
* empty/incomplete object, this read throws — surfacing the wiring bug
* loudly rather than silently rendering empty URLs.
*/
export function getRuntimeConfig(): Readonly<RuntimeConfig> {
if (typeof window === "undefined") {
// SSR phase — "use client" component bodies execute here too.
// Return placeholder; the hydration render reads the real values.
return SSR_PLACEHOLDER;
}
const cfg = window.__SHOWCASE_CONFIG__;
if (!cfg) {
// The root layout always emits the <script> tag, so a missing
// value here is a wiring bug (e.g. a route bypassed the layout,
// or the injection script ran with empty inputs). Surface it
// loudly rather than silently returning empty strings.
throw new Error(
"[runtime-config.client] window.__SHOWCASE_CONFIG__ is missing. " +
"The root layout must inject runtime config before client mount.",
);
}
// Field validation: an injection that ran with empty inputs (layout
// wired to a broken server read) yields an object that is truthy but
// useless — fail loud instead of resolving every preview iframe
// against an empty pattern. The typeof check also catches a layout
// bug injecting a non-string, which would otherwise explode far from
// the cause inside backendUrlFromPattern's replaceAll.
const value = cfg.backendHostPattern;
if (typeof value !== "string" || value.length === 0) {
throw new Error(
`[runtime-config.client] window.__SHOWCASE_CONFIG__ is incomplete: ` +
`field "backendHostPattern" is ${
typeof value === "string" ? "empty" : `of type ${typeof value}`
}. The root layout injection ran with broken inputs — check the ` +
`server-side runtime config.`,
);
}
return Object.freeze(cfg);
}
@@ -0,0 +1,90 @@
// Server-only runtime config reader. Reads from process.env at REQUEST
// time (not at module load) so a single built artifact can serve
// different URL values across staging vs prod by changing the Railway
// service's env vars — no rebuild required.
//
// `unstable_noStore()` opts the calling segment out of Next.js's static
// cache so reads always reflect the live env. Without it, a server
// component that uses this could be statically rendered at build time
// and freeze the URLs back into the artifact — defeating the runtime
// switch. See Next.js App Router docs on Dynamic Rendering.
//
// This module MUST NOT be imported from client components. The matching
// client-side reader lives in runtime-config.client.ts and reads from
// window.__SHOWCASE_CONFIG__ which the root layout injects.
import { unstable_noStore as noStore } from "next/cache";
import {
DEFAULT_BACKEND_HOST_PATTERN,
normalizeBackendHostPattern,
} from "./backend-url";
export interface RuntimeConfig {
/**
* Backend host pattern — `{slug}` is the only placeholder. Used to
* derive each integration's preview-iframe backend URL at request
* time instead of trusting the registry's `backend_url`, which is
* baked at Docker build (freezing prod hostnames into every image, so
* the staging dojo iframed prod integrations). Same semantics as
* SHOWCASE_BACKEND_HOST_PATTERN in scripts/generate-registry.ts and
* the shell's runtime config: host only, `https://` is prepended by
* the consumer (see lib/backend-url.ts).
*/
backendHostPattern: string;
}
/**
* Resolve the runtime config for shell-dojo. Called once per request by
* the root layout, which injects the result into
* window.__SHOWCASE_CONFIG__ for the client reader.
*
* `opts.noStore` (default `true`) controls whether to call
* `unstable_noStore()`. The Node.js server runtime needs the opt-out so
* Next.js does not statically prerender callers and freeze the URL
* values into the build artifact.
*/
export function getRuntimeConfig(
opts: { noStore?: boolean } = {},
): RuntimeConfig {
if (opts.noStore !== false) noStore();
// backendHostPattern is a host *pattern*, not a URL — `{slug}` must
// survive untouched. It IS normalized against common env misconfigs
// (leading scheme, trailing slash, missing `{slug}`) with warn-once
// guards — see normalizeBackendHostPattern in lib/backend-url.ts. An
// unset var defaults to the prod pattern, so a deploy with the var
// unset behaves byte-identically to the build-baked prod values.
const backendHostPattern = normalizeBackendHostPattern(
readEnvPair("SHOWCASE_BACKEND_HOST_PATTERN") ??
DEFAULT_BACKEND_HOST_PATTERN,
);
return { backendHostPattern };
}
// Env-name tolerance: deploy configs in the wild use either the bare
// name (e.g. `SHOWCASE_BACKEND_HOST_PATTERN`) or the `NEXT_PUBLIC_*`-
// prefixed name. We accept either — the primary (passed-in) name wins,
// with transparent fallback to the alternate so a Railway service
// variable set under the "wrong" name still works without redeploy.
// Same semantics as the shell's runtime-config readEnvPair.
function altEnvName(envKey: string): string {
return envKey.startsWith("NEXT_PUBLIC_")
? envKey.slice("NEXT_PUBLIC_".length)
: `NEXT_PUBLIC_${envKey}`;
}
// Length-aware env coalesce: a deliberately-empty primary (an operator
// clearing the var on a Railway service) must NOT mask a populated
// alternate. Treat empty-string as "unset" and fall through to the
// alternate. Values are .trim()ed — whitespace paste artifacts would
// otherwise survive into the pattern; a whitespace-only value counts as
// unset. The dynamic `process.env[key]` reads assume a self-hosted Node
// runtime (next start / Docker).
function readEnvPair(envKey: string): string | undefined {
const primary = process.env[envKey]?.trim();
if (primary && primary.length > 0) return primary;
const alt = process.env[altEnvName(envKey)]?.trim();
if (alt && alt.length > 0) return alt;
return undefined;
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}