Repository navigation
feat: shared domain verification and plan features - #37
Merged
Merged
Conversation
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
force-pushed
the
feat/domain-verification-and-plan-features
branch
from
November 14, 2026 05:16
7333224 to
d698e6a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 verificationFrontLit (
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 asgithub.io).isApexHostname(hostname): aware of the public suffix list (example.co.ukis 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.tldts, pinned to7.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 featurescreateBilling({ planFeatures: { business: ["learner_sso"] } }). Incloudmode every key must be a plan fromoffers.billing.hasFeature(billableEntityId, feature):ossmode: always true.cloudmode: true only when the entity's entitled paid plan includes the feature, using the same rules ascommercialState().activePaidPlan.Verification
bun run verifypasses: lint, typecheck, drift checks, all tests, build andtest:packed. New tests cover TXT matching (chunked, wrong token, timeouts), CNAME chains and loops, the apex rules, edge cases of hostname cleanup, and everyhasFeaturerule.bun.lockalso picks up workspace version fields that were already stale (0.8.1 → 0.9.0).