From 97755042cb5b6d072fb0ba069913634da54c22e9 Mon Sep 17 00:00:00 2001 From: agent-senior-engineer Date: Sat, 7 Nov 2026 21:36:44 +0000 Subject: [PATCH 1/2] nessa: publish the Terms of Use the app links at /terms (FRE-194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nessa app's Settings › Legal "Terms of Service" row had no Nessa-owned document to resolve to: it linked another product's terms, and nessa.freno.me/terms was a 404. Nessa sells an auto-renewing subscription, so App Review 3.1.2 requires the app to link terms of use as well as a privacy policy — the row has to land on a real document. - src/routes/nessa/terms.tsx: the page, in the privacy.tsx house template (EdgeCacheHeaders, site-aware PageHead, SubdomainHeader, subdomain-relative internal links). Sections cover acceptance, the service and its limits, accounts, community content and acceptable use, auto-renewable subscriptions (payment, renewal, the 24-hour cancel window, managing it in the App Store, trials, refunds, price changes), the Apple third-party acknowledgements, IP, disclaimers and liability, termination, changes, and contact. - src/lib/sitemap-routes.ts: add /terms to the Nessa set, and update the count assertion in sitemap-routes.test.ts to match. Health and safety: Nessa is not a medical device, so section 2(c) says so explicitly — the previous app-side link pointed at another product's terms, which said nothing about Nessa's training features. Deliberately left out, for Mike to decide rather than have me guess: - No legal entity name and no governing-law clause. No Nessa page states either today; the privacy policy speaks only as "we". - No NAV_CONFIG entry, so the nav link-set matrix tests are untouched. The route serves through the existing catch-all host rewrite in vercel.json; adding a nav item is a one-line change plus a matrix update if wanted. --- src/lib/sitemap-routes.test.ts | 3 +- src/lib/sitemap-routes.ts | 3 +- src/routes/nessa/terms.tsx | 446 +++++++++++++++++++++++++++++++++ 3 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 src/routes/nessa/terms.tsx diff --git a/src/lib/sitemap-routes.test.ts b/src/lib/sitemap-routes.test.ts index 66d7983..2f67797 100644 --- a/src/lib/sitemap-routes.test.ts +++ b/src/lib/sitemap-routes.test.ts @@ -67,7 +67,8 @@ describe("generateSitemap", () => { expect(locs).toContain("https://nessa.freno.me/"); expect(locs).toContain("https://nessa.freno.me/contact"); expect(locs).toContain("https://nessa.freno.me/privacy"); - expect(locs.length).toBe(3); + expect(locs).toContain("https://nessa.freno.me/terms"); + expect(locs.length).toBe(4); for (const loc of locs) { expect(loc).not.toContain("://freno.me/"); diff --git a/src/lib/sitemap-routes.ts b/src/lib/sitemap-routes.ts index 0b15819..ff4604b 100644 --- a/src/lib/sitemap-routes.ts +++ b/src/lib/sitemap-routes.ts @@ -57,7 +57,8 @@ export const SITEMAP_ROUTES: Record = { nessa: [ { path: "/", changefreq: "weekly", priority: 1.0 }, { path: "/contact", changefreq: "monthly", priority: 0.6 }, - { path: "/privacy", changefreq: "yearly", priority: 0.4 } + { path: "/privacy", changefreq: "yearly", priority: 0.4 }, + { path: "/terms", changefreq: "yearly", priority: 0.4 } ], lineage: [ diff --git a/src/routes/nessa/terms.tsx b/src/routes/nessa/terms.tsx new file mode 100644 index 0000000..e4b85f1 --- /dev/null +++ b/src/routes/nessa/terms.tsx @@ -0,0 +1,446 @@ +/** + * Nessa terms of use — `nessa.freno.me/terms`. + * + * The document behind the `Terms of Service` row in the Nessa app + * (`Nessa/Models/AppSettings.swift`, Settings › Legal). Before this page the + * row had no Nessa-owned document to resolve to: the app linked another + * product's terms, and `nessa.freno.me/terms` was a 404 (FRE-194). + * + * Nessa sells an auto-renewing subscription through the App Store, so App + * Review 3.1.2 requires the app to link a privacy policy **and** terms of + * use. Section 5 (subscriptions) and section 6 (Apple acknowledgements) are + * the clauses that requirement is read against, so keep them when editing. + * + * House voice follows `privacy.tsx`: "we" / "Nessa", no legal entity name, + * and no governing-law clause — the Nessa pages have never stated either, and + * naming an entity or a jurisdiction is a decision to make deliberately, not + * to introduce as a side effect of a link fix. + * + * PageHead is site-aware: only the base title is supplied; the ` | Nessa` + * suffix, the `https://nessa.freno.me/terms` canonical and the OG image are + * derived automatically. Internal links use public subdomain-relative paths + * (`/privacy`, `/deletion`, `/contact`) per nav-config.ts and + * page-head-meta.ts. + */ +import { A } from "@solidjs/router"; +import { PageHead } from "~/components/PageHead"; +import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders"; +import SubdomainHeader from "~/components/SubdomainHeader"; + +export default function NessaTermsOfUse() { + return ( + <> + + + +
+
Nessa's Terms of Use
+
Last Updated: November 7, 2026
+
+ Welcome to Nessa ('We', 'Us', 'Our'). + These terms of use govern your access to and use of the Nessa app and + its related services. By using Nessa you agree to these terms. Our{" "} + + privacy policy + {" "} + explains how we handle your data, and it forms part of these terms. +
+
    +
    +
    + 1. Acceptance of These Terms +
    +
    +
    +
    (a) Agreement:
    By creating an account, + downloading, or otherwise using Nessa you agree to these terms. + If you do not agree to them, do not use Nessa. +
    +
    +
    (b) Eligibility:
    You must be at least + the age of digital consent in your country to use Nessa. If you + are using Nessa with a parent's or guardian's + account, they are responsible for your compliance with these + terms. +
    +
    +
    (c) Where these terms apply:
    These + terms are between you and us. Where you obtained Nessa through + a third party such as the Apple App Store, that + party's own terms apply to your relationship with them as + well. +
    +
    +
    + +
    +
    + 2. The Service +
    +
    +
    +
    (a) What Nessa is:
    Nessa is a fitness + application for iOS, with an Apple Watch companion, that lets + you record workouts and routes, review your training history, + and take part in community features — clubs, club challenges, + posts, comments, and leaderboards. Some features are free and + some require a paid subscription (see section 5). +
    +
    +
    (b) Changes and availability:
    We are + continually improving Nessa, so features may change, move + between tiers, or be withdrawn. We do not guarantee that Nessa + will be available at all times, and we may suspend or discontinue + part or all of the service. Where a change materially affects a + paid feature you have subscribed to, we will tell you on the + subscription management page or by contact. +
    +
    +
    (c) Not medical advice:
    Nessa is not a + medical device and is not intended to diagnose, treat, cure, or + prevent any condition. Workout data, heart-rate and other + health readings, training plans, and estimates you see in Nessa + are for general information and motivation only. Consult a + qualified professional before starting or changing a training + programme, especially if you have a health condition. Never + ignore professional advice, or delay seeking it, because of + something you read in Nessa. +
    +
    +
    (d) Your surroundings:
    You are + responsible for your own safety while using Nessa, including + when recording an activity. Follow local laws and do not use + Nessa in a way that distracts you from driving or other + activities that require your full attention. +
    +
    +
    + +
    +
    + 3. Your Account +
    +
    +
    +
    (a) Sign-in:
    Nessa accounts are + authenticated through Clerk, our identity provider. You are + responsible for keeping the credentials you use to sign in + secure, and you are responsible for the activity that happens + under your account. +
    +
    +
    (b) Notify us:
    Tell us promptly, using + the contact details in section 11, if you believe someone else + has used or may use your account without your permission. +
    +
    +
    (c) One account:
    Keep to one account, + and give us accurate information so we can keep your training + history and your place in the community correct. Do not create + accounts to evade a moderation decision or to gain a fair + advantage in challenges or leaderboards. +
    +
    +
    + +
    +
    + 4. Community Content and Acceptable + Use +
    +
    +
    +
    (a) You keep your content:
    You own what + you post. By posting to a club, challenge, or other shared area + of Nessa you grant us a limited, non-exclusive licence to store, + reproduce, and display that content inside Nessa so that the + feature you used can work — for example, showing your post to + the members of your club. That licence ends when your content is + removed from our systems. We handle your content as described in + our{" "} + + privacy policy + + ; we do not sell it. +
    +
    +
    (b) What you are responsible for:
    You + are responsible for the content you post and the data you record + in Nessa, and for who you share it with. Use the privacy + settings on your profile and the privacy rules of the clubs you + join before sharing anything you would not want others to see. +
    +
    +
    (c) Do not:
    (i) post anything unlawful, + harassing, hateful, threatening, or sexually explicit involving + minors; (ii) post content you do not have the right to share, or + that infringes someone else's intellectual property; + (iii) falsify activity data, spoof a location, or otherwise + cheat a challenge, leaderboard, or competition; (iv) scrape, + bulk-download, or access Nessa through automated means we have + not authorised; (v) interfere with, overload, or probe Nessa for + vulnerabilities, or attempt to access another user's + account or data; (vi) resell, sublicense, or offer Nessa to + others as your own service. +
    +
    +
    (d) Moderation:
    We may remove or + restrict content, disqualify an entry from a challenge, or + suspend or close an account that breaks these terms. We are not + obliged to pre-monitor content. You can report content or a user + to us at any time using the contact details in section 11. +
    +
    +
    + +
    +
    + 5. Subscriptions and Payments +
    +
    +
    +
    (a) Paid tiers:
    Nessa offers paid + subscription tiers, Nessa Plus and Nessa Pro, which unlock + features described in the app. Subscriptions are sold as + auto-renewing subscriptions through the Apple App Store and are + billed to your Apple Account. +
    +
    +
    (b) Payment:
    Payment is charged to your + Apple Account at confirmation of purchase. The price of your + plan is shown in the app before you buy, and it is the price on + your App Store purchase receipt. +
    +
    +
    (c) Auto-renewal:
    Unless you cancel at + least 24 hours before the end of the current period, your + subscription renews automatically. Your account is charged for + renewal within 24 hours before the end of the current period, at + the price of the plan you selected. +
    +
    +
    (d) Managing your subscription:
    You can + turn off auto-renewal, or cancel, at any time in your App Store + account settings after purchase. Changes made there take effect + at the end of the current period, and you keep access to the + paid features you have already paid for until then. +
    +
    +
    (e) Trials:
    If we offer a free trial, + any unused portion of it is forfeited when you purchase a + subscription, and the trial converts to a paid auto-renewing + subscription unless you cancel it before it ends. +
    +
    +
    (f) Refunds:
    Purchases made through the + App Store are refunded according to Apple's refund policy + and, where applicable law gives you a right to a refund, + according to that law. Tell us using the contact details in + section 11 and we will do what we can. +
    +
    +
    (g) Price changes:
    We may change our + prices. A price change does not apply to a period you have + already paid for; it applies from your next renewal, and we give + notice where the law requires it. +
    +
    +
    (h) Downgrade:
    When a subscription + lapses or is cancelled you keep the free features, and anything + you created that belongs to a paid feature (such as a custom + route) is kept but is not editable or usable until you + subscribe again. +
    +
    +
    + +
    +
    + 6. Apple and Other Third Parties +
    +
    +
    +
    (a) Not Apple's terms:
    These terms + are between you and us, not with Apple Inc. Apple is not + responsible for Nessa or its content, and your use of Nessa must + comply with the Apple App Store Terms of Service. +
    +
    +
    (b) No Apple support:
    Apple has no + obligation to furnish maintenance and support services for + Nessa. If Nessa fails, you may notify Apple, but Apple has no + further support obligation, and support and any warranties + relating to Nessa are ours to provide as described in these + terms. +
    +
    +
    (c) Apple is not responsible for claims:
    {" "} + Apple is not responsible for product liability claims, + intellectual property infringement claims, or any other claims + relating to Nessa or your possession and use of it. We, not + Apple, are responsible for addressing any such claims and for + any claims that you may have relating to Nessa. +
    +
    +
    (d) Compliance:
    You confirm that you + are not located in a country subject to an embargo of the United + States or listed by the United States government as a + terrorist-supporting country, and that you are not listed on any + United States government list of prohibited or restricted + parties. +
    +
    +
    (e) Third-party services:
    Nessa depends + on third-party services — Clerk for authentication, Apple + HealthKit for health and workout data, Apple's In-App + Purchase system for subscriptions, and our own hosting — each + governed by its own terms. Where those terms affect how you may + use the feature they power, they apply alongside these terms. +
    +
    +
    + +
    +
    + 7. Intellectual Property +
    +
    +
    +
    (a) Our rights:
    The Nessa software, its + design, text, icons, and branding are owned by us or our + licensors and are protected by copyright and other laws. We + grant you a personal, non-exclusive, non-transferable, revocable + licence to use Nessa for your own personal, non-commercial + purposes. +
    +
    +
    (b) Restrictions:
    You may not copy, + modify, translate, distribute, sell, lease, sublicense, or + create derivative works from Nessa, and you may not reverse + engineer, decompile, or attempt to obtain its source code, + except to the extent applicable law allows this despite a + restriction. +
    +
    +
    + +
    +
    + 8. Disclaimers and Limitation of + Liability +
    +
    +
    +
    (a) As is:
    Nessa is provided "as + is" and "as available" to the fullest extent + permitted by applicable law. We do not warrant that Nessa will + be uninterrupted, secure, error-free, or that recorded data will + be accurate — GPS-derived distance, pace, elevation, and + estimates can be wrong, and device and sensor limits apply. +
    +
    +
    (b) Your data:
    You are responsible for + backing up the data you store on your device. We keep + Nessa's service data as described in the{" "} + + privacy policy + {" "} + and{" "} + + deletion page + + , but no service is a substitute for your own backup. +
    +
    +
    (c) Liability:
    To the fullest extent + permitted by law, we are not liable for indirect, incidental, + special, consequential, or punitive damages, or for lost data, + lost profits, or loss of training records, arising out of your + use of, or inability to use, Nessa. Where a court would not + allow that exclusion, our liability for claims arising from your + use of Nessa is limited to the greater of the subscription fees + you paid us in the twelve months before the event giving rise to + the claim, or the minimum the applicable law requires. Nothing + in these terms limits liability that cannot lawfully be limited, + including for fraud or for death or personal injury caused by + our negligence. +
    +
    +
    + +
    +
    + 9. Ending Your Use +
    +
    +
    +
    (a) You can leave:
    You can stop using + Nessa at any time. You can delete your account and the content + associated with it through our{" "} + + account deletion page + + . Deleting your account does not cancel a subscription that is + still in its paid period; cancel that in your App Store account + settings as described in section 5. +
    +
    +
    (b) We may end access:
    We may suspend + or close your access to Nessa if you break these terms, if we + are required to by law, or if we reasonably believe your account + has been compromised. Where we can, we will tell you why. +
    +
    +
    (c) On termination:
    Your licence to use + Nessa ends, and sections 4(a), 6, 7, 8, 10, and 11 continue to + apply. +
    +
    +
    + +
    +
    + 10. Changes to These Terms +
    +
    +
    +
    (a) Updates:
    We may update these terms + as Nessa changes. The current version is always the one at{" "} + + nessa.freno.me/terms + + , and the "Last Updated" date at the top of this page + shows when it last changed. +
    +
    +
    (b) Continued use:
    If you keep using + Nessa after a change takes effect, you accept the revised terms. + For changes that materially reduce your rights or your paid + features, we will give notice in the app or by contact before + they take effect. +
    +
    +
    + +
    +
    + 11. Contact Us +
    +
    +
    (a) Reaching Out:
    If you have any + questions or comments about these terms, want to report content, + or need help with a subscription, you can contact us{" "} + + here + + . +
    +
    +
+
+ + ); +} -- 2.49.1 From 71bb9ec5df56dbddc13e4a085d7f77d935905575 Mon Sep 17 00:00:00 2001 From: agent-cto Date: Mon, 9 Nov 2026 16:14:14 +0000 Subject: [PATCH 2/2] FRE-1670: publish /terms and the support address on the Nessa site nessa.freno.me/terms was a live 404 although AppSettings.termsOfServiceURL links it from Settings > Legal and the paywall. The document existed only on an unmerged branch and had no host-dispatch route, so it could not serve on the subdomain. Carry the Terms of Use page, add the /terms dispatcher, and publish nessa-support@freno.me on terms, privacy, contact and deletion. - src/routes/terms.tsx: host-aware dispatch for nessa; other sites 404. - src/routes/nessa/terms.tsx: the Terms page; its contact section names the support address. - privacy: add the controller clause and the support address. - contact/deletion: show the support address; deletion also accepts a request by email. - subdomain-dispatch.test.ts: list directories before filtering to .tsx files. The old predicate only ever received file names, so it asserted over an empty list and missed the missing /terms dispatch. - support-contact.test.ts: guard that every contact route renders the one address the app shows. App side unchanged: termsOfServiceURL stays https://nessa.freno.me/terms. --- src/routes/nessa/contact.tsx | 18 ++++++++++-- src/routes/nessa/content.ts | 9 ++++++ src/routes/nessa/deletion.tsx | 13 +++++++++ src/routes/nessa/privacy.tsx | 29 ++++++++++++++----- src/routes/nessa/support-contact.test.ts | 36 ++++++++++++++++++++++++ src/routes/nessa/terms.tsx | 14 +++++++-- src/routes/subdomain-dispatch.test.ts | 33 ++++++++++++++++------ src/routes/terms.tsx | 30 ++++++++++++++++++++ 8 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 src/routes/nessa/support-contact.test.ts create mode 100644 src/routes/terms.tsx diff --git a/src/routes/nessa/contact.tsx b/src/routes/nessa/contact.tsx index 6d2db7f..d06de38 100644 --- a/src/routes/nessa/contact.tsx +++ b/src/routes/nessa/contact.tsx @@ -1,6 +1,7 @@ import { ContactForm } from "~/components/ContactForm"; import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders"; import SubdomainHeader from "~/components/SubdomainHeader"; +import { SUPPORT_EMAIL } from "./content"; /** * Nessa contact page (`nessa.freno.me/contact`). @@ -16,14 +17,27 @@ import SubdomainHeader from "~/components/SubdomainHeader"; * * Acceptance: `nessa.localhost:3000/contact` renders the contact form with * Nessa branding; submissions email `michael@freno.me` with subject - * `[Nessa] Contact Request`. + * `[Nessa] Contact Request`. The support address shown by the app is also + * published here, so the contact route does not depend on the form alone + * (FRE-1670). */ export default function NessaContactPage() { return ( <> - + +

+ You can also email us directly at{" "} + + {SUPPORT_EMAIL} + + . +

+
); } diff --git a/src/routes/nessa/content.ts b/src/routes/nessa/content.ts index c127efb..a4e0df8 100644 --- a/src/routes/nessa/content.ts +++ b/src/routes/nessa/content.ts @@ -14,6 +14,15 @@ export const TAGLINE = "The fitness app that puts you first." as const; export const SUBTITLE = "Track, train, and connect — without the paywall." as const; +/** + * The support address the Nessa app shows (`SubscriptionView` / + * `HelpSupportView`). Every published Nessa document that offers a contact + * route names this same address, so a user who leaves the app to read the + * terms, privacy policy, contact page, or deletion page can still reach us + * (FRE-1670). Keep in step with `nessa-support@freno.me` in the app. + */ +export const SUPPORT_EMAIL = "nessa-support@freno.me" as const; + export const ICON_DEFAULT = "/Nessa Exports/Nessa-iOS-Default-1024x1024.png" as const; export const ICON_DARK = "/Nessa Exports/Nessa-iOS-Dark-1024x1024.png" as const; diff --git a/src/routes/nessa/deletion.tsx b/src/routes/nessa/deletion.tsx index cbd0d2e..fd76ed1 100644 --- a/src/routes/nessa/deletion.tsx +++ b/src/routes/nessa/deletion.tsx @@ -33,6 +33,7 @@ import { DELETION_GRACE_PERIOD_LABEL, PAGE_META } from "~/routes/nessa/deletion-content"; +import { SUPPORT_EMAIL } from "./content"; export default function NessaDeletionPage() { return ( @@ -54,6 +55,18 @@ export default function NessaDeletionPage() { removed. No data related to the account is retained in any way. +
+ Prefer email? Send your deletion request from the address on your + Nessa account to{" "} + + {SUPPORT_EMAIL} + + ; the same {DELETION_GRACE_PERIOD_LABEL} grace period applies. +
+ 5. Contact Us
-
(a) Reaching Out:
If there are any - questions or comments regarding this privacy policy, you can - contact us{" "} - - here - - . +
(a) Data Controller:
Nessa is operated by + FrenoCorp. For the purposes of applicable data-protection law, + FrenoCorp is the controller of the personal information described + in this policy. +
+
(b) Reaching Out:
If there are any + questions or comments regarding this privacy policy, or you want + to access, correct, or delete your personal information, you can + contact us{" "} + + through the contact form + {" "} + or email{" "} + + {SUPPORT_EMAIL} + + . +
diff --git a/src/routes/nessa/support-contact.test.ts b/src/routes/nessa/support-contact.test.ts new file mode 100644 index 0000000..880a805 --- /dev/null +++ b/src/routes/nessa/support-contact.test.ts @@ -0,0 +1,36 @@ +/** + * Regression guard for the published Nessa support address (FRE-1670). + * + * The app tells users to mail `nessa-support@freno.me` from the subscription + * screen and Help & Support. Before this guard, no published Nessa page named + * that address: a user who left the app to read the terms, privacy policy, + * contact page, or deletion page found no way to reach support. That is a + * privacy-policy contact gap (GDPR/CCPA-style) and an App Review question + * waiting to happen. + * + * The address lives in one place (`SUPPORT_EMAIL`), and every page that + * offers a contact route must render it. Reading the route sources is + * deliberate: the defect is a page that exists but does not mention the + * address, which no runtime assertion over a single page can observe. + */ +import { describe, it, expect } from "bun:test"; +import { readFileSync } from "fs"; +import { SUPPORT_EMAIL } from "./content"; + +const CONTACT_ROUTE_FILES = [ + "src/routes/nessa/terms.tsx", + "src/routes/nessa/privacy.tsx", + "src/routes/nessa/contact.tsx", + "src/routes/nessa/deletion.tsx" +]; + +describe("Nessa published support address", () => { + it("is the address the app shows", () => { + expect(SUPPORT_EMAIL).toBe("nessa-support@freno.me"); + }); + + it.each(CONTACT_ROUTE_FILES)("%s renders it", (file) => { + const source = readFileSync(file, "utf8"); + expect(source).toContain("SUPPORT_EMAIL"); + }); +}); diff --git a/src/routes/nessa/terms.tsx b/src/routes/nessa/terms.tsx index e4b85f1..a442f92 100644 --- a/src/routes/nessa/terms.tsx +++ b/src/routes/nessa/terms.tsx @@ -26,6 +26,7 @@ import { A } from "@solidjs/router"; import { PageHead } from "~/components/PageHead"; import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders"; import SubdomainHeader from "~/components/SubdomainHeader"; +import { SUPPORT_EMAIL } from "./content"; export default function NessaTermsOfUse() { return ( @@ -38,7 +39,7 @@ export default function NessaTermsOfUse() {
Nessa's Terms of Use
-
Last Updated: November 7, 2026
+
Last Updated: November 9, 2026
Welcome to Nessa ('We', 'Us', 'Our'). These terms of use govern your access to and use of the Nessa app and @@ -434,8 +435,15 @@ export default function NessaTermsOfUse() { questions or comments about these terms, want to report content, or need help with a subscription, you can contact us{" "} - here - + through the contact form + {" "} + or email{" "} + + {SUPPORT_EMAIL} + .
diff --git a/src/routes/subdomain-dispatch.test.ts b/src/routes/subdomain-dispatch.test.ts index a8efd65..f07251f 100644 --- a/src/routes/subdomain-dispatch.test.ts +++ b/src/routes/subdomain-dispatch.test.ts @@ -14,7 +14,10 @@ import { describe, it, expect } from "bun:test"; import { readdirSync, existsSync, statSync } from "fs"; function isRouteFile(name: string): boolean { - return name.endsWith(".tsx") || name.endsWith(".ts"); + // Page routes are components; the `.ts` files beside them are pure content / + // metadata modules (content.ts, meta.ts, *-content.ts) with no default + // export the router can render, so they are not dispatch targets. + return name.endsWith(".tsx"); } function listRouteFiles(dir: string): string[] { @@ -25,6 +28,20 @@ function listRouteFiles(dir: string): string[] { } } +function listDirectories(dir: string): string[] { + try { + return readdirSync(dir).filter((name) => { + try { + return statSync(`${dir}/${name}`).isDirectory(); + } catch { + return false; + } + }); + } catch { + return []; + } +} + function hasDispatchRoute(routeName: string, routesDir: string): boolean { // Check for src/routes/.tsx if (existsSync(`${routesDir}/${routeName}.tsx`)) { @@ -41,14 +58,12 @@ describe("subdomain routing dispatch invariant", () => { const routesDir = "./src/routes"; it("every subdomain route has a top-level dispatch route", () => { - const prefixDirs = listRouteFiles(routesDir) - .filter((name) => { - try { - return statSync(`${routesDir}/${name}`).isDirectory(); - } catch { - return false; - } - }) + // List directory entries before filtering to route files. The previous + // shape filtered with `listRouteFiles` first, which only ever returns + // `.tsx`/`.ts` names, so `statSync(...).isDirectory()` was never true + // and the assertion ran over an empty list — it passed while `/terms` + // had no dispatch file (FRE-1670). + const prefixDirs = listDirectories(routesDir) // Exclude non-product subdomains .filter((name) => ["nessa", "lineage", "gaze", "inputhalo", "nook"].includes(name) diff --git a/src/routes/terms.tsx b/src/routes/terms.tsx new file mode 100644 index 0000000..98a716a --- /dev/null +++ b/src/routes/terms.tsx @@ -0,0 +1,30 @@ +import { Switch, Match } from "solid-js"; +import { useSite } from "~/context/SiteContext"; +import NotFound from "./[...404]"; +import NessaTermsOfUse from "./nessa/terms"; + +/** + * Host-aware terms-of-use route — `/terms`. + * + * The public browser path `/terms` is what the Nessa app links from + * Settings › Legal and from the subscription paywall + * (`AppSettings.termsOfServiceURL`). Production Vercel deployments do not + * apply the `vercel.json` host rewrites (the Nitro Build Output API config + * supersedes them), so the subdomain page needs this top-level dispatch file + * to be reachable — without it `nessa.freno.me/terms` is a 404 even though + * `src/routes/nessa/terms.tsx` exists. + * + * Only Nessa publishes terms of use today. Every other site — and the main + * site — falls back to the 404 handler rather than borrowing another + * product's legal document. + */ +export default function TermsPage() { + const site = useSite(); + return ( + }> + + + + + ); +} -- 2.49.1