Pure code motion; Finder.tsx 754 → 347 lines. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XStQKxPfEjrTvWo83KFCxG
143 lines
7.1 KiB
Markdown
143 lines
7.1 KiB
Markdown
# names — Business Name Finder
|
|
|
|
Free business-name generator with live domain availability, at
|
|
**names.poweredbyotm.com**. A first-party **Powered by OTM** app.
|
|
|
|
- **Stack:** Next.js 15 (App Router, `output: "standalone"`), React 18, no DB,
|
|
no local accounts, public. Sign in with OTM (SSO) exists only to gate AI. Generation runs **client-side**; availability is a server
|
|
route (browsers can't do DNS, and RDAP servers send no CORS headers).
|
|
- **Repo:** `tonym/names` on `git.poweredbyotm.com`.
|
|
- **Deploy:** platform MCP — `build_app("names")` then `deploy_app("names")`.
|
|
NOT docker-compose-from-this-repo. Prod deploys need Tony's explicit OK.
|
|
|
|
`README.md` explains the mechanisms in full. This file is the house rules.
|
|
|
|
## Deploy
|
|
|
|
Registered in `platform/apps/otm-admin/lib/first-party-apps.ts` as
|
|
`publicAccess: true`, `needsDb: false`, `needsAuthSecret: true` (only for
|
|
`OTM_SSO_SECRET`/`AUTH_SECRET` — see below), `needsSuggestions: true`,
|
|
`needsAnthropic: true`, `uploads: "/app/public/uploads"`.
|
|
|
|
Adding this app to the registry required an otm-admin release (0.113.0) — and
|
|
because the auto-deploy webhook is **gated on green CI**, `names` does not
|
|
appear in `/apps` until platform CI passes and otm-admin redeploys (~2.5 min).
|
|
`build_app` before that point fails on an unknown app id.
|
|
|
|
**`build_app` times out at the MCP layer and keeps building.** That's normal, not
|
|
a failure. Poll for the image instead of retrying the call:
|
|
|
|
```sh
|
|
ssh debian@51.81.80.250 'docker images platform-names --format "{{.Repository}} {{.CreatedSince}}"'
|
|
```
|
|
|
|
Verify a deploy against the LIVE site, never the deploy log:
|
|
|
|
```sh
|
|
curl -s https://names.poweredbyotm.com/api/health
|
|
curl -s -X POST https://names.poweredbyotm.com/api/availability \
|
|
-H 'content-type: application/json' \
|
|
-d '{"domains":["google.com","zzq7xk4m.io"]}' # expect taken + unverified-available
|
|
```
|
|
|
|
## Availability — the trap
|
|
|
|
**Never route lookups through `rdap.org`.** Its redirector returns a bare `404`
|
|
both for "this domain is unregistered" and for "I have no RDAP server for this
|
|
TLD" — the two are indistinguishable from the status code. Routing through it
|
|
would report every `.io` as available. We resolve the TLD ourselves from IANA's
|
|
bootstrap (`data.iana.org/rdap/dns.json`, cached 24h) and query the
|
|
authoritative server directly, so a `404` can only mean unregistered.
|
|
|
|
The TLDs with no RDAP at all (`.io .co .me .sh .gg .us .de`) fall back to a DNS
|
|
`NS` lookup. That path can only ever return `unverified-available`, shown amber
|
|
as *probably free* — a registered-but-undelegated domain looks identical. **Do
|
|
not "upgrade" it to a green check.**
|
|
|
|
We're an anonymous client against other people's public endpoints. Keep the
|
|
manners: 60 domains/request, 8-wide concurrency, per-IP token bucket, 10-minute
|
|
result cache.
|
|
|
|
## Generation — three guards that must not be relaxed
|
|
|
|
`src/lib/generate.ts` is pure and deterministic. The strategies are deliberately
|
|
cheap and dumb; **quality lives in `scoreName`** — tune the scorer before adding
|
|
vocabulary. Each of these exists because of specific bad output, and removing it
|
|
brings that output straight back:
|
|
|
|
- `expandKeywords` matches category hints by **prefix, never substring**. Plain
|
|
containment let `repair` match the `ai` category (rep-**ai**-r) and dragged
|
|
neural-network words into an auto-shop brief.
|
|
- `syncopate` only drops the schwa from a final `-er` (`flicker` → `flickr`).
|
|
The original "drop the last vowel" gave `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 list. Without it the top 20 came
|
|
back as eighteen coinages — raw score ranking clumps badly.
|
|
|
|
## Shared OTM widgets need Tailwind
|
|
|
|
The bottom-right feedback chat-pop is `SuggestionLightbulb` from
|
|
`@otm/account-panel`, styled with Tailwind utilities. This app's own UI is plain
|
|
CSS, and for a while it had no Tailwind at all — so the widget rendered as a
|
|
bare unstyled button (issue #2). Tailwind is configured with `preflight: false`
|
|
(it must not reset the handwritten CSS) and its `content` includes
|
|
`node_modules/@otm/account-panel/src`. Element-level rules in `globals.css` are
|
|
scoped to `.wrap`, and the widget mounts *outside* `.wrap`, so neither side can
|
|
restyle the other. Keep it that way when adding global CSS.
|
|
|
|
## Client layout
|
|
|
|
`components/Finder.tsx` is orchestration only (inputs → generate → lookup).
|
|
Cards are `NameCard.tsx`, the Saved panel `Wishlist.tsx`, the AI switch
|
|
`AiControls.tsx`; session and shortlist state are `hooks/useOtmSession.ts` and
|
|
`hooks/useWishlist.ts`; the availability result shape + batch fetch is
|
|
`lib/availability-client.ts`. Add UI to the piece it belongs to, not to Finder.
|
|
|
|
## Wishlist is browser-only
|
|
|
|
`src/lib/wishlist.ts` — `localStorage` key `names.wishlist`, capped at 60. It
|
|
snapshots the last-seen status per domain; the Finder keeps snapshots fresh from
|
|
live lookups and **Re-check** re-queries through the normal availability route.
|
|
No server state, on purpose.
|
|
|
|
## AI — two paths, one brief
|
|
|
|
`src/lib/ai-prompt.ts` holds the prompt + parser both paths share; change the
|
|
brief there, never in one path.
|
|
|
|
1. **Platform gateway (default, needs OTM sign-in).** The platform registers
|
|
this app `needsAnthropic` and injects `ANTHROPIC_BASE_URL` (its metered
|
|
gateway) + `ANTHROPIC_API_KEY` (this app's *gateway token* `otmapp_names_…`,
|
|
NOT an sk-ant key — see `platform/apps/otm-admin/lib/first-party-ai.ts`).
|
|
`app/api/ai-names` calls it server-side, **only with a session cookie**, plus
|
|
~20 requests/hour per OTM account. Usage is metered against the "Powered By
|
|
OTM" customer, so the platform's monthly cap on `/settings/ai` is the hard
|
|
stop. **The login gate is what makes it safe to hold a metered credential on
|
|
a free public tool — do not add an anonymous path to `/api/ai-names`.**
|
|
2. **Bring your own key (override).** `localStorage` `names.aiKey`, called
|
|
browser-direct — same pattern as `rpo`'s Google Vision key. The
|
|
`anthropic-dangerous-direct-browser-access` header is correct *here* and
|
|
would be wrong in a product that owns the key. When a key is saved, the UI
|
|
shows only this path.
|
|
|
|
## Sign in with OTM — no accounts here
|
|
|
|
`src/lib/otm.ts` (`app:names` audience, bounce URLs) → OTM's `/sso/authorize`
|
|
→ `app/api/auth/otm-sso` verifies the 60s ticket (`src/lib/sso-ticket.ts`,
|
|
vendored verbatim from `@otm/account-panel` — keep in sync) and mints our own
|
|
HMAC cookie (`src/lib/session.ts`, `names.session`, 12h, signed with
|
|
`AUTH_SECRET`). jti replay guard is an in-memory Map — fine for one container
|
|
and 60s tickets. No user rows, no DB. `/api/auth/session` tells the UI who's
|
|
signed in and whether platform AI is wired (`platformAi`); if the env is
|
|
missing the feature is simply absent.
|
|
|
|
## Registrar links
|
|
|
|
`src/lib/registrars.ts`. Links are plain today (every `affiliateCode` is `""`).
|
|
Fill one in and `HAS_AFFILIATE_LINKS` flips the footer disclosure on
|
|
automatically. Keep the code and the disclosure in that one file — a disclosure
|
|
that can drift out of sync with whether links actually pay is worse than none.
|