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
82 changes: 64 additions & 18 deletions apps/web/app/api/docs/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,29 @@ function getSnippet(content: string, query: string, length = 160): string {
return snippet
}

export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams.get('q')?.trim() ?? ''
type IndexedDoc = {
title: string
href: string
section: string
plainContent: string
lowerTitle: string
lowerContent: string
}

if (query.length < 2) {
return NextResponse.json([] as SearchResult[])
}
/**
* The docs corpus, parsed once per process.
*
* Every request used to `existsSync` + `readFileSync` + `gray-matter` + regex
* strip every doc file, on a route with no rate limit that the search UI calls
* on a 200ms debounce. The content is build-time static - it cannot change
* while the server is running - so there is no reason to redo any of it.
*/
let corpus: IndexedDoc[] | null = null

const lowerQuery = query.toLowerCase()
const results: (SearchResult & { score: number })[] = []
function getCorpus(): IndexedDoc[] {
if (corpus) return corpus

const docs: IndexedDoc[] = []
for (const section of docSections) {
for (const item of section.items) {
const slug = item.href.replace('/docs/', '').split('/')
Expand All @@ -63,26 +76,59 @@ export async function GET(request: NextRequest) {
const raw = fs.readFileSync(filePath, 'utf-8')
const { data: fm, content } = matter(raw)
const title = (fm.title as string) || item.title

const titleMatch = title.toLowerCase().includes(lowerQuery)
const plainContent = stripMarkdown(content)
const contentMatch = plainContent.toLowerCase().includes(lowerQuery)

if (!titleMatch && !contentMatch) continue

const snippet = contentMatch ? getSnippet(plainContent, query) : plainContent.slice(0, 140).trim() + '…'

results.push({
docs.push({
title,
href: item.href,
section: section.title,
snippet,
matchInTitle: titleMatch,
score: titleMatch ? 10 : 1,
plainContent,
lowerTitle: title.toLowerCase(),
lowerContent: plainContent.toLowerCase(),
})
}
}

corpus = docs
return corpus
}

/**
* Longest query we will scan the corpus for. Past this a query cannot match
* anything meaningful, and the length is attacker-controlled.
*/
const MAX_QUERY_LENGTH = 128

export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams.get('q')?.trim().slice(0, MAX_QUERY_LENGTH) ?? ''

if (query.length < 2) {
return NextResponse.json([] as SearchResult[])
}

const lowerQuery = query.toLowerCase()
const results: (SearchResult & { score: number })[] = []

for (const doc of getCorpus()) {
const titleMatch = doc.lowerTitle.includes(lowerQuery)
const contentMatch = doc.lowerContent.includes(lowerQuery)

if (!titleMatch && !contentMatch) continue

const snippet = contentMatch
? getSnippet(doc.plainContent, query)
: doc.plainContent.slice(0, 140).trim() + '…'

results.push({
title: doc.title,
href: doc.href,
section: doc.section,
snippet,
matchInTitle: titleMatch,
score: titleMatch ? 10 : 1,
})
}

results.sort((a, b) => b.score - a.score)

return NextResponse.json(
Expand Down
31 changes: 26 additions & 5 deletions apps/web/app/api/webhook-sample/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,39 @@ export async function POST(req: Request) {
});
}

// Nothing this endpoint accepts is large. Reading an unbounded body - and
// then HMAC-ing over an unbounded caller-supplied secret - is free CPU for
// anyone who asks, on a route that exists only to show what a payload looks
// like. Both are capped well above any legitimate input.
const MAX_BODY_BYTES = 4_096;
const MAX_SECRET_LENGTH = 256;
const MAX_ADDRESS_LENGTH = 56;

let body: Body = {};
try {
if (req.headers.get("content-type")?.includes("application/json")) {
body = (await req.json()) as Body;
const raw = await req.text();
if (Buffer.byteLength(raw, "utf8") > MAX_BODY_BYTES) {
return Response.json(
{
error: "payload_too_large",
message: `Request body is capped at ${MAX_BODY_BYTES} bytes.`,
},
{ status: 413 },
);
}
body = raw ? (JSON.parse(raw) as Body) : {};
}
} catch {
/* allow empty body */
/* allow empty or malformed body */
}

const secret = body.secret?.trim() || `whsec_demo_${randomBytes(16).toString("hex")}`;
const generatedSecret = !body.secret?.trim();
const address = body.address?.trim() || "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV";
const callerSecret = body.secret?.trim().slice(0, MAX_SECRET_LENGTH);
const secret = callerSecret || `whsec_demo_${randomBytes(16).toString("hex")}`;
const generatedSecret = !callerSecret;
const address =
body.address?.trim().slice(0, MAX_ADDRESS_LENGTH) ||
"GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV";

const event = generateSamplePayment(address);
const payload = JSON.stringify(event);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const instrumentSerif = Instrument_Serif({
});

export const metadata: Metadata = {
title: "Orbit Stellar - Real-time event infrastructure for Stellar developers",
title: "Orbital - Real-time event infrastructure for Stellar developers",
description:
"Watch any Stellar address. Register webhooks. React hooks for on-chain events. The missing event layer for Stellar developers.",
};
Expand Down
95 changes: 52 additions & 43 deletions apps/web/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

import Link from 'next/link'

import {
FOOTER_COMMUNITY_LINKS,
FOOTER_PRODUCT_LINKS,
NPM_PACKAGES,
npmUrl,
type NavLink,
} from '@/lib/links'

const labelStyle: React.CSSProperties = {
fontFamily: 'var(--font-sans)',
fontSize: '11px',
Expand All @@ -22,6 +30,20 @@ const linkStyle: React.CSSProperties = {
transition: 'color 0.15s',
}

function FooterLink({ label, href, external }: NavLink) {
return (
<Link
href={href}
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
style={linkStyle}
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--muted2)')}
>
{label}
</Link>
)
}

export default function Footer() {
return (
<footer style={{ borderTop: '1px solid var(--border)', padding: '80px 52px 0' }}>
Expand All @@ -46,7 +68,7 @@ export default function Footer() {
marginBottom: '12px',
}}
>
Orbit Stellar
Orbital
</p>
<p
style={{
Expand All @@ -69,72 +91,59 @@ export default function Footer() {
>
MIT License
</p>
<p
style={{
fontFamily: 'var(--font-sans)',
fontSize: '12px',
color: 'var(--muted2)',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<span style={{ color: 'var(--accent)' }}>●</span>
All systems operational
</p>
{/*
A hardcoded "● All systems operational" badge used to sit here,
next to a "Status" link that went nowhere. There is no uptime
monitor behind it, so it was green even while the demo endpoint
was returning 503. Removed rather than faked - put it back only
alongside a real status source.
*/}
</div>

{/* Product */}
<div>
<span style={labelStyle}>Product</span>
{['Docs', 'SDKs', 'How it works', 'Changelog', 'Status'].map((item) => (
<Link
key={item}
href="#"
style={linkStyle}
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--muted2)')}
>
{item}
</Link>
{FOOTER_PRODUCT_LINKS.map((link) => (
<FooterLink key={link.label} {...link} />
))}
</div>

{/* Packages */}
<div>
<span style={labelStyle}>Packages</span>
{[
'npm i @orbital-stellar/pulse-webhooks',
'npm i @orbital-stellar/pulse-notify',
].map((cmd) => (
<p
key={cmd}
{/*
All six published packages, each linked to its npm page. This
listed only two of them as unlinked plain text, which understated
what is actually shipped and gave the reader nowhere to go.
*/}
{NPM_PACKAGES.map((pkg) => (
<Link
key={pkg}
href={npmUrl(pkg)}
target="_blank"
rel="noopener noreferrer"
style={{
fontFamily: 'var(--font-mono)',
fontSize: '13px',
color: 'var(--muted2)',
textDecoration: 'none',
display: 'block',
marginTop: '12px',
transition: 'color 0.15s',
}}
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--muted2)')}
>
{cmd}
</p>
@orbital-stellar/{pkg}
</Link>
))}
</div>

{/* Community */}
<div>
<span style={labelStyle}>Community</span>
{['GitHub', 'Twitter', 'SCF Grant', 'Open an issue'].map((item) => (
<Link
key={item}
href="#"
style={linkStyle}
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--muted2)')}
>
{item}
</Link>
{FOOTER_COMMUNITY_LINKS.map((link) => (
<FooterLink key={link.label} {...link} />
))}
</div>
</div>
Expand All @@ -157,7 +166,7 @@ export default function Footer() {
color: 'var(--muted)',
}}
>
© 2026 Orbit Stellar
© 2026 Orbital
</span>
<span
style={{
Expand Down
8 changes: 6 additions & 2 deletions apps/web/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import Link from "next/link";
import { motion } from "framer-motion";

import { GITHUB_REPO } from "@/lib/links";

const ease = [0.22, 1, 0.36, 1] as const;

const fadeUp = (delay: number) => ({
Expand Down Expand Up @@ -85,7 +87,7 @@ export default function Hero() {
style={{ display: "flex", gap: "12px", flexWrap: "wrap", justifyContent: "center" }}
>
<Link
href="#"
href="/docs"
style={{
background: "var(--accent)",
color: "#000",
Expand All @@ -100,7 +102,9 @@ export default function Hero() {
Read the docs
</Link>
<Link
href="#"
href={GITHUB_REPO}
target="_blank"
rel="noopener noreferrer"
style={{
background: "transparent",
color: "#fff",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/HowItWorks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const STEPS = [
},
{
num: "03",
title: "Orbit Stellar",
title: "Orbital",
description: "Filters by address, normalizes the payload, and routes to subscribers.",
},
{
Expand All @@ -23,7 +23,7 @@ const STEPS = [

export default function HowItWorks() {
return (
<section style={{ padding: "120px 32px" }}>
<section id="how-it-works" style={{ padding: "120px 32px" }}>
<div style={{ maxWidth: "var(--max-width)", margin: "0 auto" }}>
<h2
style={{
Expand Down
Loading
Loading