Skip to content
Open
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
8 changes: 7 additions & 1 deletion app/(app)/developers/apps/new/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export async function createAppAction(formData: FormData) {

const clientId = `app_${randomToken(16)}`;
const clientSecret = `sec_${randomToken(32)}`;
// The api key (activation/bearer API) and the OAuth client secret guard
// different surfaces, so they are independent secrets - leaking one must not
// authenticate the other. The api key satisfies the not-null column here; it
// is the request-bearer flow that issues an api key to the user.
const apiKey = `sec_${randomToken(32)}`;
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") + "-" + randomToken(4);

const row = await queryOne<{ id: string }>(
Expand All @@ -51,11 +56,12 @@ export async function createAppAction(formData: FormData) {
oauth_profile_version,
status,
owner_user_id
) values ($1, $2, $3, $4, $4, $5, 'confidential', 'client_secret_post', $6, $7, true, 'bn-oauth-2026-05', 'active', $8) returning id`,
) values ($1, $2, $3, $4, $5, $6, 'confidential', 'client_secret_post', $7, $8, true, 'bn-oauth-2026-05', 'active', $9) returning id`,
[
clientId,
name,
slug,
hashToken(apiKey),
hashToken(clientSecret),
[redirectUri],
["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:device_code"],
Expand Down
86 changes: 86 additions & 0 deletions app/(info)/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Metadata } from "next";
import Link from "next/link";
import { DocHeader, DocSection, DocList } from "@/components/Doc";

export const metadata: Metadata = {
title: "Documentation — bottleneck",
description:
"Get started with bottleneck: Telegram-confirmed sign-in, passkeys, and an OAuth 2.1 / OpenID Connect provider.",
};

export default function DocsPage() {
return (
<article>
<DocHeader title="Documentation" subtitle="Get started and integrate with bottleneck" />

<DocSection heading="What bottleneck is">
<p>
bottleneck is an account service with Telegram-confirmed two-factor sign-in and passkeys,
and a full OAuth 2.1 / OpenID Connect provider you can use to sign users into your own
applications.
</p>
</DocSection>

<DocSection heading="Getting started">
<DocList
items={[
<>
<Link href="/register" className="text-accent-strong transition-colors hover:text-fg">
Create an account
</Link>{" "}
and confirm it from Telegram to finish sign-up.
</>,
"Add a passkey under Password & 2FA to sign in without a password.",
"Each sign-in is approved from your linked Telegram, so a stolen password alone is not enough.",
]}
/>
</DocSection>

<DocSection heading="For developers">
<p>
bottleneck implements OAuth 2.1 and OpenID Connect. Client metadata is published at the
standard discovery endpoint:
</p>
<p>
<code className="text-[13px] text-accent-strong">/.well-known/openid-configuration</code>
</p>
<DocList
items={[
"Register an application from the developer dashboard to obtain a client ID and client secret.",
"Use the authorization, token, and userinfo endpoints listed in the discovery document; PKCE is required.",
"Request only the scopes your app needs (for example openid, profile, email). Users approve scopes on a consent screen.",
]}
/>
</DocSection>

<DocSection heading="API bearer tokens">
<p>
For server-to-server access you can request a long-lived API bearer token. Each request is
reviewed before a token is issued. API keys and OAuth client secrets are separate
credentials — keep both confidential and rotate them if exposed.
</p>
</DocSection>

<DocSection heading="SDKs">
<p>
Official client libraries are available for Node.js and Go to verify tokens and call the
API. See the SDK directories in the project for usage.
</p>
</DocSection>

<DocSection heading="More">
<p>
Browse the{" "}
<Link href="/faq" className="text-accent-strong transition-colors hover:text-fg">
FAQ
</Link>{" "}
for common questions, or reach{" "}
<Link href="/support" className="text-accent-strong transition-colors hover:text-fg">
support
</Link>{" "}
if you are stuck.
</p>
</DocSection>
</article>
);
}
14 changes: 9 additions & 5 deletions app/(app)/faq/page.tsx → app/(info)/faq/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Metadata } from "next";
import { CircleHelp, ShieldCheck, Boxes } from "lucide-react";
import { Section } from "@/components/Section";

export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "FAQ — bottleneck",
description: "Answers to common questions about bottleneck accounts, security, and OAuth apps.",
};

function QA({ q, a }: { q: string; a: string }) {
return (
Expand All @@ -23,15 +27,15 @@ export default function FaqPage() {
<Section title="Account" icon={CircleHelp}>
<QA
q="How do I create an account?"
a="Choose Create account on the sign-in page, fill in your details, and confirm with Telegram to finish."
a="Choose Create account on the sign-in page, fill in your details, accept the Terms, Privacy Policy, and Rules, and confirm with Telegram to finish."
/>
<QA
q="Where do I update my profile?"
a="Open Profile in the sidebar to edit your name, bio, and the details you share with connected apps."
a="Open your Profile from your account to edit your name, bio, and the details you share with connected apps."
/>
<QA
q="How do I sign out everywhere?"
a="Go to Sessions and choose Revoke others to end every session except the one you are using."
a="Open Sessions in your account and choose Revoke others to end every session except the one you are using."
/>
</Section>

Expand All @@ -53,7 +57,7 @@ export default function FaqPage() {
<Section title="Developers" icon={Boxes}>
<QA
q="How do I connect an app with OAuth?"
a="Register an app under Developer → OAuth apps, then use the authorization and token endpoints. The OAuth docs page has the full reference and a test lab."
a="Register an app from the developer dashboard, then use the authorization and token endpoints. The documentation page has the discovery endpoint and the full reference."
/>
<QA
q="What is an API bearer token?"
Expand Down
60 changes: 60 additions & 0 deletions app/(info)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Link from "next/link";
import { TERMS_EFFECTIVE, TERMS_VERSION } from "@/lib/server/terms";

// Public chrome for the information pages. These are reachable without a
// session, so they carry their own header/footer rather than the app shell.

function Monogram() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true" className="shrink-0">
<path d="M3 1 V7 L8 12 V19" stroke="var(--accent)" strokeWidth="1.5" />
<path d="M17 1 V7 L12 12 V19" stroke="var(--accent)" strokeWidth="1.5" />
<path d="M8 12 H12" stroke="var(--accent)" strokeWidth="1.5" />
</svg>
);
}

const NAV = [
{ href: "/docs", label: "Docs" },
{ href: "/faq", label: "FAQ" },
{ href: "/terms", label: "Terms" },
{ href: "/privacy", label: "Privacy" },
{ href: "/rules", label: "Rules" },
];

export default function InfoLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex flex-col bg-canvas">
<header className="border-b border-rule bg-card">
<div className="mx-auto flex h-14 w-full max-w-[820px] items-center justify-between px-6">
<Link href="/" className="flex select-none items-center gap-2">
<Monogram />
<span className="text-[15px] font-semibold tracking-tight text-fg">bottleneck</span>
</Link>
<nav className="flex items-center gap-4 text-[13px]">
{NAV.map(item => (
<Link
key={item.href}
href={item.href}
className="text-secondary transition-colors hover:text-fg"
>
{item.label}
</Link>
))}
</nav>
</div>
</header>

<main className="mx-auto w-full max-w-[820px] flex-1 px-6 py-12">{children}</main>

<footer className="border-t border-rule">
<div className="mx-auto flex w-full max-w-[820px] flex-wrap items-center justify-between gap-3 px-6 py-6 text-[12px] text-faint">
<span>bottleneck · {new Date().getFullYear()}</span>
<span>
Terms version {TERMS_VERSION} · effective {TERMS_EFFECTIVE}
</span>
</div>
</footer>
</div>
);
}
100 changes: 100 additions & 0 deletions app/(info)/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Metadata } from "next";
import Link from "next/link";
import { DocHeader, DocSection, DocList } from "@/components/Doc";
import { TERMS_EFFECTIVE } from "@/lib/server/terms";

export const metadata: Metadata = {
title: "Privacy Policy — bottleneck",
description: "How bottleneck collects, uses, and protects your data.",
};

export default function PrivacyPage() {
return (
<article>
<DocHeader title="Privacy Policy" subtitle={`Effective ${TERMS_EFFECTIVE}`} />

<DocSection heading="What we collect">
<DocList
items={[
"Account details you provide: first name, username, optional bio, email address, and an optional date of birth.",
"Authentication data: a hash of your password (never the password itself), registered passkeys, and your linked Telegram account id and username.",
"Security and session data: IP address, user agent, approximate country, and sign-in and account-security events, used to protect your account.",
]}
/>
</DocSection>

<DocSection heading="How we use it">
<DocList
items={[
"To create and operate your account and to sign you in.",
"To provide two-factor approval and other security features, and to detect and prevent abuse.",
"To notify you about security events affecting your account, where you have those notifications enabled.",
]}
/>
</DocSection>

<DocSection heading="Service providers">
<p>We use a small number of processors to run the Service:</p>
<DocList
items={[
"Telegram — to deliver two-factor approval prompts and to support Telegram-based sign-in.",
"An email provider — to send verification and security codes to your email address.",
"Cloudflare Turnstile — to protect sign-up and sign-in against automated abuse.",
]}
/>
</DocSection>

<DocSection heading="Sharing with connected apps">
<p>
When you authorize a third-party application through OAuth, we share only the information
covered by the scopes you approve (for example your profile, email, or date of birth).
We do not sell your personal data.
</p>
</DocSection>

<DocSection heading="Retention">
<p>
We keep your account data for as long as your account exists. Security event records are
retained for a limited period to investigate abuse. When you request deletion, your
account enters a short grace period and is then permanently removed; some records may be
retained where required for security or by law.
</p>
</DocSection>

<DocSection heading="Your choices">
<DocList
items={[
"Export your data or delete your account at any time from Settings → Danger zone.",
"Deactivate your account to pause it; signing back in reactivates it.",
"Control profile visibility and notification preferences in your settings.",
]}
/>
</DocSection>

<DocSection heading="Changes">
<p>
We may update this Privacy Policy. Material changes are reflected in the version and
effective date shown here.
</p>
</DocSection>

<DocSection heading="Contact">
<p>
For privacy questions, reach us through{" "}
<Link href="/support" className="text-accent-strong transition-colors hover:text-fg">
support
</Link>
. See also the{" "}
<Link href="/terms" className="text-accent-strong transition-colors hover:text-fg">
Terms of Service
</Link>{" "}
and{" "}
<Link href="/rules" className="text-accent-strong transition-colors hover:text-fg">
Rules
</Link>
.
</p>
</DocSection>
</article>
);
}
Loading