Skip to content

feat: shared domain verification and plan features - #37

Merged
clbotdev merged 2 commits into
mainfrom
feat/domain-verification-and-plan-features
Nov 14, 2026
Merged

clbotdev merged 2 commits into
mainfrom
feat/domain-verification-and-plan-features

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Two generic pieces that CourseLit's learner SSO needs, added here so every CodeLit app can use them instead of keeping its own copy.

@codelitdev/platform: custom-domain verification

FrontLit (apps/api/src/domains/queries.ts) and CourseLit (apps/api/src/school-hosts.ts) each carry a near-identical copy of custom-domain verification. This adds one shared implementation with the same behaviour:

  • normalizeHostname(input, { allowIpLiteral?, allowPublicSuffix? }): lowercase, IDN to punycode, no trailing dot. It refuses ports, paths, credentials, invalid labels and single-label names, and by default also refuses IP literals and public suffixes (including private ones such as github.io).
  • isApexHostname(hostname): aware of the public suffix list (example.co.uk is an apex).
  • issueOwnershipToken(): 16 random bytes, hex, the format both apps use today.
  • createDomainVerification({ label, resolver? }) returns three functions:
    • ownershipRecord: the TXT record to publish. Its value is the bare token, so records customers have already published keep working.
    • verifyOwnership: checks that TXT record.
    • verifyRouting: follows the CNAME chain (stopping on a loop or after 16 hops). An apex can instead use CNAME flattening or A records matching a configured set.
  • createDnsResolver({ servers?, timeoutMs? }) can pin trusted recursive DNS servers, and every lookup times out (5 s by default). Ownership checks are only as trustworthy as the resolver.
  • New dependency: tldts, pinned to 7.4.19.

Product changes for FrontLit and CourseLit are listed in the changeset. Storage and product rules (apex/www pairing, one domain per school) stay in each app.

@codelitdev/billing: plan features

  • createBilling({ planFeatures: { business: ["learner_sso"] } }). In cloud mode every key must be a plan from offers.
  • billing.hasFeature(billableEntityId, feature):
    • oss mode: always true.
    • cloud mode: true only when the entity's entitled paid plan includes the feature, using the same rules as commercialState().activePaidPlan.
    • An unknown feature, no paid plan, or any error reading state returns false.

Verification

bun run verify passes: lint, typecheck, drift checks, all tests, build and test:packed. New tests cover TXT matching (chunked, wrong token, timeouts), CNAME chains and loops, the apex rules, edge cases of hostname cleanup, and every hasFeature rule.

bun.lock also picks up workspace version fields that were already stale (0.8.1 → 0.9.0).

Adds normalizeHostname, isApexHostname, issueOwnershipToken,
createDnsResolver and createDomainVerification so FrontLit and CourseLit
can drop their copies. Lookups take an injectable resolver that can pin
trusted DNS servers and time out; public suffixes come from tldts.
createBilling takes planFeatures, and hasFeature answers from the same
entitlement as activePaidPlan. It is always true in oss mode and fails
closed on unknown features, no paid plan or read errors.
@clbotdev
clbotdev force-pushed the feat/domain-verification-and-plan-features branch from 7333224 to d698e6a Compare November 14, 2026 05:16
@clbotdev
clbotdev merged commit 688664f into main Nov 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants