From 78460d35c90403f5105d8cad1c147ab2a5b2a1c8 Mon Sep 17 00:00:00 2001 From: Tony Moon Date: Mon, 10 Aug 2026 13:40:26 -0500 Subject: [PATCH] Initial release: business-name finder with live domain availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Free, no-account tool for naming a business and finding a domain you can actually register. Availability uses no paid API and no key. IANA's RDAP bootstrap maps ~1,200 TLDs to their authoritative registry servers (404 = free, 200 = taken); the handful with no RDAP at all (.io, .co, .me, .sh, .gg, .us) fall back to a DNS NS lookup and are reported as "probably free" rather than confirmed, because a registered-but-undelegated domain is indistinguishable that way. Name generation is pure, deterministic, and client-side — eight strategies over a curated word bank, ranked by a scorer that does the real quality work. Three of its guards (prefix-only hint matching, -er-only syncopation, truncation rejection) exist because of specific bad output and should not be relaxed. Optional AI suggestions use the visitor's OWN Anthropic/Gemini key from localStorage, called browser-direct, so this stays free to run. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JChWdFJPCRxMBxErb8kUVK --- .dockerignore | 7 + .gitignore | 9 + .npmrc | 1 + Dockerfile | 41 + README.md | 92 ++ next.config.mjs | 16 + package-lock.json | 1210 +++++++++++++++++ package.json | 24 + public/.gitkeep | 0 src/app/api/availability/route.ts | 85 ++ src/app/api/health/route.ts | 5 + .../suggestions/[number]/comments/route.ts | 19 + src/app/api/suggestions/route.ts | 9 + src/app/api/suggestions/upload/route.ts | 21 + src/app/changelog/page.tsx | 26 + src/app/globals.css | 374 +++++ src/app/layout.tsx | 47 + src/app/page.tsx | 14 + src/app/settings/page.tsx | 107 ++ src/components/Finder.tsx | 425 ++++++ src/lib/ai.ts | 144 ++ src/lib/generate.ts | 448 ++++++ src/lib/rdap.ts | 215 +++ src/lib/registrars.ts | 64 + src/lib/suggestions.ts | 27 + src/lib/tlds.ts | 60 + src/lib/version.ts | 25 + src/lib/wordbank.ts | 108 ++ tsconfig.json | 21 + 29 files changed, 3644 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/.gitkeep create mode 100644 src/app/api/availability/route.ts create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/suggestions/[number]/comments/route.ts create mode 100644 src/app/api/suggestions/route.ts create mode 100644 src/app/api/suggestions/upload/route.ts create mode 100644 src/app/changelog/page.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/settings/page.tsx create mode 100644 src/components/Finder.tsx create mode 100644 src/lib/ai.ts create mode 100644 src/lib/generate.ts create mode 100644 src/lib/rdap.ts create mode 100644 src/lib/registrars.ts create mode 100644 src/lib/suggestions.ts create mode 100644 src/lib/tlds.ts create mode 100644 src/lib/version.ts create mode 100644 src/lib/wordbank.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3ebfb0e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +.git +.tmp +.env +npm-debug.log +.DS_Store diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95947aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/node_modules +/.next +/out +/build +/.tmp +next-env.d.ts +*.tsbuildinfo +.env +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c53a3c0 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@otm:registry=https://git.poweredbyotm.com/api/packages/tonym/npm/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e7434b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# syntax=docker/dockerfile:1.7 + +# ---------- deps ---------- +FROM node:20-bookworm-slim AS deps +WORKDIR /app +COPY .npmrc ./ +COPY package.json package-lock.json* ./ +RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi + +# ---------- build ---------- +FROM node:20-bookworm-slim AS builder +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# Ensure public/ exists even if the repo ships it empty — Next standalone's +# runner stage COPYs it, and Docker errors on a missing source path. +RUN mkdir -p public && npm run build + +# ---------- runtime ---------- +FROM node:20-bookworm-slim AS runner +WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 +RUN apt-get update && apt-get install -y --no-install-recommends wget \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1 + +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..87f120d --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# names.poweredbyotm.com + +Free business-name finder: type a few keywords, get brandable name ideas, and see +which domains are actually free — checked against the registries themselves. + +- **Stack:** Next.js 15 (App Router, `output: "standalone"`), React 18, no DB, no + auth, public. Name generation runs **client-side**; availability is a server + route because browsers can't do DNS and RDAP servers send no CORS headers. +- **Repo:** `tonym/names` on `git.poweredbyotm.com`. +- **Deploy:** OTM platform — first-party app (`/apps`), built + deployed via the + platform MCP, not docker-compose-from-this-repo. + +## How availability works — and what it costs (nothing) + +There is no paid domain API here and no key to manage. + +1. **RDAP** (RFC 7482), the registries' own replacement for WHOIS. IANA + publishes a bootstrap file at `data.iana.org/rdap/dns.json` mapping every TLD + to its authoritative RDAP server. We cache it for 24h, resolve the TLD + ourselves, and query that server directly — `404` means unregistered, `200` + means registered. Covers ~1,200 TLDs including `.com/.net/.org/.ai/.dev/.app`. +2. **DNS `NS` lookup**, for the TLDs that publish no RDAP server at all — `.io`, + `.co`, `.me`, `.sh`, `.gg`, `.us`, `.de`. NXDOMAIN strongly suggests + unregistered, but a registered domain with no delegated nameservers looks + identical. These report `unverified-available` and the UI shows them in amber + as *probably free*, never as a confirmed green. + +**Do not "simplify" this by pointing everything at `rdap.org`.** Its redirector +returns a bare `404` both for "this domain is free" and for "I have no RDAP +server for this TLD" — the two cases are indistinguishable from the status code, +which is exactly the bug that would report every `.io` as available. + +Manners, since we're an anonymous client against other people's public +endpoints: 60 domains max per request, 8-wide concurrency, a per-IP token +bucket, and a 10-minute result cache. + +## Name generation + +`src/lib/generate.ts` is pure and deterministic — the same +`(keywords, style, seed)` always yields the same list, so re-renders don't +reshuffle results and a shared URL reproduces what the sender saw. + +Eight strategies (compound, suffix-word, action, blend, coined, clipped, root, +domain hack) over the vocabulary in `src/lib/wordbank.ts`. The strategies are +cheap and dumb on purpose — **quality comes from `scoreName`**, so tune the +scorer before adding more words. + +Three guards exist because of specific bad output, and removing them brings it +straight back: + +- `expandKeywords` matches category hints by **prefix, never substring** — plain + containment let `repair` match the `ai` category (rep-**ai**-r) and drag + neural-network vocabulary into an auto-shop brief. +- `syncopate` only drops the schwa from a final `-er` (`flicker` → `flickr`). + The original "drop the last vowel" produced `crema` → `crem`, `kettle` → + `kettl`, `security` → `securit`. +- `isTruncation` rejects any candidate that is a strict prefix of a word the + generator knows. This is what keeps stumps out of the results. + +`diversify()` caps each strategy at ~28% of the returned list. Without it the +top 20 came back as eighteen coinages — scoring alone clumps badly. + +## Bring-your-own AI key + +Optional. `/settings` stores an Anthropic or Gemini key in `localStorage` +(`names.aiKey`) and the browser calls that provider **directly** — the key never +touches our server. Same pattern as `rpo`'s Google Vision key, for the same +reason: the platform has no per-app env tool, and this is a free public tool that +shouldn't hold a metered credential. + +The `anthropic-dangerous-direct-browser-access` header is correct here and would +be wrong in a product that owns the key. The distinction is whose key it is. + +## Registrar links + +`src/lib/registrars.ts`. Links are plain today — every `affiliateCode` is `""`. +Fill one in and `HAS_AFFILIATE_LINKS` flips, which turns on the footer +disclosure automatically. Keeping both in one file is deliberate: a disclosure +that can drift out of sync with whether links actually pay is worse than none. + +## Local development + +```bash +npm install +npm run dev # http://localhost:3000 +npm run typecheck +npm run build +``` + +No env vars are needed to run it. The suggestions lightbulb degrades to a quiet +"not configured" state unless `GITEA_URL` / `GITEA_TOKEN` / `GITEA_REPO` are set +(the platform injects those at deploy time). diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..068d158 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,16 @@ +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "standalone", + transpilePackages: ["@otm/account-panel"], + // Pin the standalone trace root to this app (a stray lockfile in $HOME made + // Next infer the wrong workspace root, which can drop files from the + // standalone bundle). Same guard rpo carries. + outputFileTracingRoot: __dirname, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7fc7472 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1210 @@ +{ + "name": "names-poweredbyotm", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "names-poweredbyotm", + "version": "0.1.0", + "dependencies": { + "@otm/account-panel": "0.32.0", + "next": "^15.1.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@types/node": "^22.9.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.3" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.23.tgz", + "integrity": "sha512-Mv3Z9hVbFcPnoLevsZ6rnX1TBtyHb5E17yN7HTPDXSXxeNsGBjUFrdbjRXKKXIOhfth7/cg6Ay7PZ2UFawaWsQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.23.tgz", + "integrity": "sha512-SrEwOROH/rhA03F59hHtdhgtfZMWGzr5duDBWgRQt2rS3mJhqMKOcnNx6txOd0/i3E3D3uFKYFvyHsEiwQxzag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.23.tgz", + "integrity": "sha512-f0FpFbG2EhDCuptBGcfrLcYMDuQAhe6m1QA4VVfXFrIBoFXvXt/olGbBkYkloKlXQtmhuzvtdYyuu/6zf07GIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.23.tgz", + "integrity": "sha512-WlNtfepUXKX2u2ZsJZ8c3c8+tJSRZqsYzoMwLOY72A8ucKCCgxgNhiePA3qzFYahVWrwcQd8jOeJmBinc+VFVQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.23.tgz", + "integrity": "sha512-W/6qKk7UG93mg14PmQC+2urt69MIdwTBLNQ6MJyeC4wOCIHCjz+VfgssvS1pK7mgYBtLC1g6VKNoHD9xB0WWGg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.23.tgz", + "integrity": "sha512-vzefI32mi6VMk96RaTAyxApgfGbiFzQBXVsekEjsDv1fr48mlABTWx0sUYhaYCBHWqCalxmz3DxbxFcbFvzNtw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.23.tgz", + "integrity": "sha512-qppK/3dTGOTI+aoWWBZc3DshFIhrzgL8guATlaN9V6M1QJxbkP/rhEZ22tdICsQ/2WWXopMZ2Jokzj2u3uKY3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.23.tgz", + "integrity": "sha512-Wc29KFOdT7XBcII3Vtmw7aoU8Uk3Mes/FNJfhFeSHdYBFJWMcR/DsI8U9BCPUhq/uycsUVuqSKGthW15tLsigA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.23.tgz", + "integrity": "sha512-/C7wRW4fa9s/PKA18zGPPpVmx8ycgVpP8yOxro4gzGTzjPJdscbAP3ODeFvgiIovxD176Z2J/SXO9t8PJKHLeQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@otm/account-panel": { + "version": "0.32.0", + "resolved": "https://git.poweredbyotm.com/api/packages/tonym/npm/%40otm%2Faccount-panel/-/0.32.0/account-panel-0.32.0.tgz", + "integrity": "sha512-nWzVbaUC3Cv9awr8eTKB8XZv38GVXMnfXZPoqo6+jf+8GPHNwrpPjk0dtfV9wOq/FZyo686CF4CLoLBrRrh3nQ==", + "peerDependencies": { + "@stripe/react-stripe-js": ">=2", + "@stripe/stripe-js": ">=3", + "next": ">=14", + "next-auth": ">=4", + "react": ">=18", + "stripe": ">=18" + }, + "peerDependenciesMeta": { + "@stripe/react-stripe-js": { + "optional": true + }, + "@stripe/stripe-js": { + "optional": true + }, + "stripe": { + "optional": true + } + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.23.tgz", + "integrity": "sha512-Gvd2WKgvxIXCGotxcI1im/Uf3rS3J3oZGw0g/uskg6AVBZhyE3aAbujkYWzS3xLmEPEtTLfkaVQUKK0KMTSIkA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.23", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.23", + "@next/swc-darwin-x64": "15.5.23", + "@next/swc-linux-arm64-gnu": "15.5.23", + "@next/swc-linux-arm64-musl": "15.5.23", + "@next/swc-linux-x64-gnu": "15.5.23", + "@next/swc-linux-x64-musl": "15.5.23", + "@next/swc-win32-arm64-msvc": "15.5.23", + "@next/swc-win32-x64-msvc": "15.5.23", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "4.24.15", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.15.tgz", + "integrity": "sha512-NnjYtjrSOAx/TIVFGTX4IfI/9yHnNpi4B7FuLUwuV20v2Zxgr2OGP/YN0ynJuI7y8QOnTBPitfOdEXZrVvhIuA==", + "license": "ISC", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^11.1.1" + }, + "peerDependencies": { + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT", + "peer": true + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "peer": true, + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT", + "peer": true + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..469e243 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "names-poweredbyotm", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "next start --port 3000", + "lint": "next lint", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@otm/account-panel": "0.32.0", + "next": "^15.1.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@types/node": "^22.9.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "typescript": "^5.6.3" + } +} diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/availability/route.ts b/src/app/api/availability/route.ts new file mode 100644 index 0000000..f0b3168 --- /dev/null +++ b/src/app/api/availability/route.ts @@ -0,0 +1,85 @@ +// POST /api/availability { domains: string[] } -> { results: DomainCheck[] } +// +// Server-side because browsers can't do DNS and most RDAP servers send no CORS +// headers. We are an anonymous client against other people's public registry +// endpoints, so this route is deliberately stingy: a hard per-request cap, a +// per-IP token bucket, and bounded concurrency inside checkDomains(). + +import type { NextRequest } from "next/server"; +import { checkDomains } from "@/lib/rdap"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; // node:dns — not available on edge + +/** Most a single request may ask for. A full page of results is ~40. */ +const MAX_DOMAINS = 60; + +// --- Per-IP token bucket ----------------------------------------------------- +// In-memory, so it resets on redeploy and doesn't coordinate across replicas. +// That's fine: this is a courtesy throttle to keep one enthusiastic tab from +// hammering Verisign, not a security control. + +const RATE_CAPACITY = 240; // domains +const RATE_REFILL_PER_SEC = 4; +const buckets = new Map(); + +function clientIp(req: NextRequest): string { + const fwd = req.headers.get("x-forwarded-for"); + if (fwd) return fwd.split(",")[0].trim(); + return req.headers.get("x-real-ip") ?? "unknown"; +} + +/** Returns true when the request may proceed, spending `cost` tokens. */ +function takeTokens(ip: string, cost: number): boolean { + const now = Date.now(); + const b = buckets.get(ip) ?? { tokens: RATE_CAPACITY, at: now }; + const refill = ((now - b.at) / 1000) * RATE_REFILL_PER_SEC; + b.tokens = Math.min(RATE_CAPACITY, b.tokens + refill); + b.at = now; + + if (b.tokens < cost) { + buckets.set(ip, b); + return false; + } + b.tokens -= cost; + if (buckets.size > 10000) buckets.clear(); + buckets.set(ip, b); + return true; +} + +export async function POST(req: NextRequest) { + let body: unknown; + try { + body = await req.json(); + } catch { + return Response.json({ error: "Expected a JSON body" }, { status: 400 }); + } + + const raw = (body as { domains?: unknown } | null)?.domains; + if (!Array.isArray(raw)) { + return Response.json({ error: "Expected { domains: string[] }" }, { status: 400 }); + } + + const domains = [ + ...new Set( + raw + .filter((d): d is string => typeof d === "string") + .map((d) => d.trim().toLowerCase()) + .filter((d) => d.length > 0 && d.length <= 253), + ), + ].slice(0, MAX_DOMAINS); + + if (!domains.length) { + return Response.json({ results: [] }); + } + + if (!takeTokens(clientIp(req), domains.length)) { + return Response.json( + { error: "Slow down a moment — too many lookups from this address." }, + { status: 429, headers: { "retry-after": "30" } }, + ); + } + + const results = await checkDomains(domains); + return Response.json({ results }); +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..8652ecc --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,5 @@ +export const dynamic = "force-dynamic"; + +export function GET() { + return Response.json({ ok: true, service: "names" }); +} diff --git a/src/app/api/suggestions/[number]/comments/route.ts b/src/app/api/suggestions/[number]/comments/route.ts new file mode 100644 index 0000000..c36efd2 --- /dev/null +++ b/src/app/api/suggestions/[number]/comments/route.ts @@ -0,0 +1,19 @@ +import type { NextRequest } from "next/server"; +import { createSuggestionCommentsRoute } from "@otm/account-panel/server"; +import { giteaConfig, suggestionGuard } from "@/lib/suggestions"; + +export const dynamic = "force-dynamic"; + +// The factory types `params` as `{number}|Promise<{number}>` so it can mount on +// either Next 14 or 15; Next 15's generated route validator demands a strict +// `Promise<...>`. These thin re-typed re-exports exist only to satisfy that +// signature check — they add no logic. (Same shape as rpo's.) +const handlers = createSuggestionCommentsRoute({ + gitea: giteaConfig(), + guard: suggestionGuard, +}); + +type Ctx = { params: Promise<{ number: string }> }; + +export const GET = (req: NextRequest, ctx: Ctx) => handlers.GET(req, ctx); +export const POST = (req: NextRequest, ctx: Ctx) => handlers.POST(req, ctx); diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts new file mode 100644 index 0000000..a0332ae --- /dev/null +++ b/src/app/api/suggestions/route.ts @@ -0,0 +1,9 @@ +import { createSuggestionsRoute } from "@otm/account-panel/server"; +import { giteaConfig, suggestionGuard } from "@/lib/suggestions"; + +export const dynamic = "force-dynamic"; + +export const { GET, POST } = createSuggestionsRoute({ + gitea: giteaConfig(), + guard: suggestionGuard, +}); diff --git a/src/app/api/suggestions/upload/route.ts b/src/app/api/suggestions/upload/route.ts new file mode 100644 index 0000000..0d94422 --- /dev/null +++ b/src/app/api/suggestions/upload/route.ts @@ -0,0 +1,21 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { createSuggestionUploadRoute } from "@otm/account-panel/server"; +import { suggestionGuard } from "@/lib/suggestions"; + +export const dynamic = "force-dynamic"; + +// Pasted screenshots land in this app's own public/uploads/suggestions/ and the +// issue body embeds an origin-relative URL — no shared upload service. The +// registry entry mounts /app/public/uploads as a bind mount so these survive a +// container recreate. +export const { POST } = createSuggestionUploadRoute({ + guard: suggestionGuard, + save: async ({ name, bytes }) => { + const safe = `${Date.now()}-${name.replace(/[^a-zA-Z0-9._-]/g, "_")}`; + const dir = path.join(process.cwd(), "public", "uploads", "suggestions"); + await mkdir(dir, { recursive: true }); + await writeFile(path.join(dir, safe), Buffer.from(bytes)); + return { url: `/uploads/suggestions/${safe}` }; + }, +}); diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx new file mode 100644 index 0000000..beff81e --- /dev/null +++ b/src/app/changelog/page.tsx @@ -0,0 +1,26 @@ +import Link from "next/link"; +import { CHANGELOG } from "@/lib/version"; + +export const metadata = { title: "Changelog · Business Name Finder" }; + +export default function ChangelogPage() { + return ( + <> +

Changelog

+

+ ← Back to the name finder +

+ {CHANGELOG.map((entry) => ( +
+

v{entry.version}

+ {entry.date} +
    + {entry.changes.map((c, i) => ( +
  • {c}
  • + ))} +
+
+ ))} + + ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..49ee66c --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,374 @@ +:root { + --bg: #0b0d10; + --panel: #14181d; + --panel-2: #1a1f26; + --panel-border: #232a31; + --text: #e7e9ec; + --muted: #93a1ad; + --faint: #6b7885; + --accent: #4f9cf9; + --accent-dim: #2b6fd0; + --ok: #3ecf8e; + --ok-bg: #10281f; + --warn: #f5b942; + --warn-bg: #33290f; + --taken: #5a646e; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--accent); +} + +.wrap { + max-width: 980px; + margin: 0 auto; + padding: 32px 20px 80px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.brand { + font-size: 13px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); +} + +h1 { + font-size: 30px; + margin: 4px 0 6px; + letter-spacing: -0.02em; +} + +h2 { + font-size: 19px; + margin: 32px 0 10px; +} + +.tagline { + color: var(--muted); + margin: 0 0 26px; + font-size: 15px; + line-height: 1.5; + max-width: 62ch; +} + +/* ---------- form ---------- */ + +.field { + margin-bottom: 16px; +} + +.field label { + display: block; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + margin-bottom: 7px; +} + +input[type="text"], +input[type="password"], +textarea, +select { + width: 100%; + background: var(--panel); + color: var(--text); + border: 1px solid var(--panel-border); + border-radius: 10px; + padding: 12px 14px; + font-size: 16px; + font-family: inherit; + line-height: 1.4; +} + +input:focus, +textarea:focus, +select:focus { + outline: none; + border-color: var(--accent-dim); +} + +.hint { + font-size: 13px; + color: var(--faint); + margin-top: 6px; + line-height: 1.5; +} + +button { + font-family: inherit; + font-size: 15px; + cursor: pointer; + border-radius: 9px; + border: 1px solid var(--panel-border); + background: var(--panel-2); + color: var(--text); + padding: 11px 18px; +} + +button:hover:not(:disabled) { + border-color: var(--accent-dim); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +button.primary { + background: var(--accent-dim); + border-color: var(--accent-dim); + color: #fff; + font-weight: 600; +} + +button.primary:hover:not(:disabled) { + background: var(--accent); + border-color: var(--accent); +} + +.controls { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + margin: 18px 0 8px; +} + +/* ---------- style + tld pickers ---------- */ + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.chip { + font-size: 14px; + padding: 7px 13px; + border-radius: 999px; + border: 1px solid var(--panel-border); + background: var(--panel); + color: var(--muted); +} + +.chip[aria-pressed="true"] { + border-color: var(--accent-dim); + background: #16283f; + color: var(--text); +} + +.chip .chip-note { + color: var(--faint); + font-size: 12px; + margin-left: 6px; +} + +/* ---------- results ---------- */ + +.results { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 10px; + margin-top: 14px; +} + +.card { + background: var(--panel); + border: 1px solid var(--panel-border); + border-radius: 12px; + padding: 13px 15px; + display: flex; + flex-direction: column; + gap: 9px; +} + +.card-name { + font-size: 18px; + font-weight: 600; + letter-spacing: -0.01em; + word-break: break-all; +} + +.card-name .tld { + color: var(--muted); + font-weight: 400; +} + +.card-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + font-size: 13px; +} + +.status { + display: inline-flex; + align-items: center; + gap: 6px; + font-weight: 600; + font-size: 13px; +} + +.status.available { + color: var(--ok); +} + +.status.unverified-available { + color: var(--warn); +} + +.status.taken { + color: var(--taken); +} + +.status.unknown, +.status.checking { + color: var(--faint); +} + +.card.is-taken { + opacity: 0.55; +} + +.strategy { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--faint); +} + +.register-link { + font-size: 13px; + text-decoration: none; + white-space: nowrap; +} + +.register-link:hover { + text-decoration: underline; +} + +/* ---------- notices ---------- */ + +.notice { + border-radius: 10px; + padding: 12px 14px; + font-size: 14px; + line-height: 1.55; + margin: 16px 0; + border: 1px solid; +} + +.notice.warn { + background: var(--warn-bg); + border-color: #6a5418; + color: #f2dfae; +} + +.notice.info { + background: var(--panel); + border-color: var(--panel-border); + color: var(--muted); +} + +.empty { + color: var(--muted); + font-size: 15px; + padding: 34px 0; + text-align: center; +} + +/* ---------- footer ---------- */ + +.footer { + margin-top: 56px; + padding-top: 18px; + border-top: 1px solid var(--panel-border); + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 18px; + flex-wrap: wrap; +} + +.foot-note { + font-size: 12.5px; + color: var(--faint); + max-width: 62ch; + line-height: 1.55; +} + +.foot-meta { + display: flex; + gap: 14px; + align-items: center; + font-size: 13px; + white-space: nowrap; +} + +.version-chip { + border: 1px solid var(--panel-border); + border-radius: 999px; + padding: 3px 10px; + color: var(--muted); + text-decoration: none; + font-size: 12px; +} + +/* ---------- changelog ---------- */ + +.entry { + border-left: 2px solid var(--panel-border); + padding-left: 16px; + margin-bottom: 26px; +} + +.entry h3 { + margin: 0 0 4px; + font-size: 16px; +} + +.entry .date { + color: var(--faint); + font-size: 13px; +} + +.entry ul { + margin: 10px 0 0; + padding-left: 18px; + color: var(--muted); + line-height: 1.6; + font-size: 14.5px; +} + +@media (max-width: 560px) { + h1 { + font-size: 25px; + } + .results { + grid-template-columns: 1fr; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..d382660 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import Link from "next/link"; +import { SuggestionLightbulb } from "@otm/account-panel"; +import { VERSION } from "@/lib/version"; +import { HAS_AFFILIATE_LINKS } from "@/lib/registrars"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Business Name Finder · Powered by OTM", + description: + "Free business name generator with live domain availability, checked against the registries themselves over RDAP. No account, no credit card.", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + +
+
+ + Powered by OTM + + +
+ {children} +
+ + Availability comes from each registry’s own RDAP service, and from a DNS + check for the few extensions that publish none. It’s a strong signal, not a + reservation — a name is only yours once a registrar confirms the purchase. + {HAS_AFFILIATE_LINKS ? ( + <> Some registrar links earn us a commission at no extra cost to you. + ) : null} + + + Settings + + v{VERSION} + + +
+
+ + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..ba9b5a0 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,14 @@ +import Finder from "@/components/Finder"; + +export default function HomePage() { + return ( + <> +

Find a name you can actually register

+

+ Describe the business, get brandable name ideas, and see which domains are free — + checked against the registries themselves, live. No account, no credit card, no trial. +

+ + + ); +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..4f8fceb --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { loadAiSettings, saveAiSettings, type AiProvider } from "@/lib/ai"; + +export default function SettingsPage() { + const [provider, setProvider] = useState("anthropic"); + const [key, setKey] = useState(""); + const [saved, setSaved] = useState(false); + const [hasStored, setHasStored] = useState(false); + + useEffect(() => { + const existing = loadAiSettings(); + if (existing) { + setProvider(existing.provider); + setKey(existing.key); + setHasStored(true); + } + }, []); + + const onSave = () => { + const trimmed = key.trim(); + saveAiSettings(trimmed ? { provider, key: trimmed } : null); + setHasStored(Boolean(trimmed)); + setSaved(true); + window.setTimeout(() => setSaved(false), 2500); + }; + + const onClear = () => { + saveAiSettings(null); + setKey(""); + setHasStored(false); + setSaved(true); + window.setTimeout(() => setSaved(false), 2500); + }; + + return ( + <> +

Settings

+

+ ← Back to the name finder +

+ +

Bring your own AI key (optional)

+

+ The name finder works fully offline without this — the built-in generator needs no + key and costs nothing. Adding your own API key gets you a second, usually more + imaginative set of suggestions alongside it. +

+ +

+ Your key is stored in this browser only and the request goes straight + from your browser to {provider === "anthropic" ? "Anthropic" : "Google"}. It is never sent + to our server and never appears in our logs. Usage is billed to your own account, so use + a key with a spending limit set. +

+ +
+ + +

+ {provider === "anthropic" ? ( + <> + Create a key at{" "} + + console.anthropic.com + + . Starts with sk-ant-. + + ) : ( + <> + Create a key at{" "} + + aistudio.google.com + + . Gemini has a free tier that is plenty for this. + + )} +

+
+ +
+ + setKey(e.target.value)} + /> +
+ +
+ + {hasStored ? : null} + {saved ? Saved to this browser. : null} +
+ + ); +} diff --git a/src/components/Finder.tsx b/src/components/Finder.tsx new file mode 100644 index 0000000..4152680 --- /dev/null +++ b/src/components/Finder.tsx @@ -0,0 +1,425 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { generateNames, scoreName, type Idea, type Style } from "@/lib/generate"; +import { DEFAULT_TLDS, TLDS, UNVERIFIABLE_TLDS } from "@/lib/tlds"; +import { DEFAULT_REGISTRAR, REGISTRARS, registerUrl } from "@/lib/registrars"; +import { generateWithAi, loadAiSettings, type AiSettings } from "@/lib/ai"; + +type Availability = "available" | "taken" | "unverified-available" | "unknown"; + +interface DomainCheck { + domain: string; + status: Availability; + method: "rdap" | "dns" | "none"; + detail?: string; +} + +/** How many names to put on screen. Each one costs `selectedTlds.length` + * registry lookups, so this is the main dial on how hard we lean on the + * registries — keep it modest. */ +const NAME_COUNT = 24; + +/** Domains per availability request. The route caps at 60; staying under it + * means results paint in progressively instead of in one late lump. */ +const BATCH = 40; + +const STYLES: Array<{ id: Style; label: string; note: string }> = [ + { id: "balanced", label: "Balanced", note: "a bit of everything" }, + { id: "real", label: "Real words", note: "northbakery, axlekit" }, + { id: "coined", label: "Coined", note: "novault, tensora" }, + { id: "short", label: "Short", note: "brevity first" }, +]; + +export default function Finder() { + const [keywords, setKeywords] = useState(""); + const [style, setStyle] = useState