ReviewOS

also looking at this

stacks/lastsoulultra

LSU's own coaching surface, and what auditing the site turned up

#12
Merged glennmichael123 wants to merge claude/last-soul-ultra-web-6e0718 into main
86 files +3943 -1190

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

resources/functions/coaching-brand.tsadded+182-0
Changes to resources/functions/coaching-brand.ts
@@ -0,0 +1,182 @@
1/**
2 * The published brand, read from HQ's one public endpoint.
3 *
4 * `GET {HQ}/api/brands/{slug}` is the only coaching endpoint this site may
5 * call: everything else is account-scoped and its tokens never cross the
6 * embedding boundary. What comes back is another service's JSON, so nothing
7 * here trusts it. Every field is checked, anything unexpected drops the whole
8 * payload, and a null brand renders the draft state rather than a half page.
9 *
10 * The published theme is deliberately NOT read. HQ applies it inside its own
11 * frame, and this site's chrome is LSU's - black, #dd0f15, Anton over Barlow -
12 * held in CSS where it cannot depend on a request completing. A site whose
13 * palette arrives over the network paints unstyled when that network is slow.
14 */
15
16export interface CoachingProduct {
17 id: number
18 name: string
19 description: string
20 priceCents: number
21 currency: string
22 interval: string
23 trialDays: number
24}
25
26export interface CoachingMethod {
27 id: number
28 name: string
29 summary: string
30 version: number
31 /** Where a runner reaches this coach directly. Always absolute https. */
32 contactUrl: string | null
33}
34
35export interface CoachingOffer {
36 label: string
37 method: CoachingMethod
38 coach: string
39 product: CoachingProduct
40}
41
42export interface CoachingBrand {
43 slug: string
44 name: string
45 headline: string
46 description: string
47 /** 'en' or 'de'. HQ publishes no other locale. */
48 locale: 'en' | 'de'
49 /** HQ module keys the brand has enabled. */
50 modules: string[]
51 offers: CoachingOffer[]
52 supportUrl: string | null
53 privacyUrl: string | null
54 termsUrl: string | null
55}
56
57function text(value: unknown, max = 400): string | null {
58 return typeof value === 'string' && value.trim().length > 0 && value.length <= max ? value.trim() : null
59}
60
61function count(value: unknown): number | null {
62 return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : null
63}
64
65/**
66 * An absolute http(s) URL, or null. A relative or javascript: URL from a
67 * remote payload has no business reaching an href on this site.
68 */
69function link(value: unknown): string | null {
70 if (typeof value !== 'string') return null
71 try {
72 const url = new URL(value)
73 return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString() : null
74 }
75 catch {
76 return null
77 }
78}
79
80/** ISO 4217, upper case. Anything else and the price is not printed. */
81function currency(value: unknown): string | null {
82 return typeof value === 'string' && /^[A-Z]{3}$/.test(value) ? value : null
83}
84
85function parseOffer(raw: unknown): CoachingOffer | null {
86 if (!raw || typeof raw !== 'object') return null
87 const row = raw as Record<string, any>
88
89 const label = text(row.label, 80)
90 const methodName = text(row.method?.name, 120)
91 const methodSummary = text(row.method?.summary, 600)
92 const methodId = count(row.method?.id)
93 const version = count(row.method?.version)
94 const productId = count(row.product?.id)
95 const productName = text(row.product?.name, 120)
96 const priceCents = count(row.product?.price_cents)
97 const unit = currency(row.product?.currency)
98 const interval = text(row.product?.billing_interval, 16)
99
100 if (label === null || methodName === null || methodSummary === null) return null
101 if (methodId === null || version === null || productId === null) return null
102 if (productName === null || priceCents === null || unit === null || interval === null) return null
103
104 return {
105 label,
106 coach: text(row.coach?.name, 120) ?? 'The coach',
107 method: { id: methodId, name: methodName, summary: methodSummary, version, contactUrl: link(row.method?.contact_url) },
108 product: {
109 id: productId,
110 name: productName,
111 description: text(row.product?.description, 600) ?? '',
112 priceCents,
113 currency: unit,
114 interval,
115 trialDays: count(row.product?.trial_days) ?? 0,
116 },
117 }
118}
119
120/**
121 * The published brand, or null.
122 *
123 * Null covers every failure the same way on purpose: an unpublished brand, an
124 * HQ that is down, and a payload this site does not recognise all mean the
125 * same thing to a visitor, which is that there is nothing to buy yet.
126 */
127export function parseBrand(raw: unknown, expectedSlug: string): CoachingBrand | null {
128 if (!raw || typeof raw !== 'object') return null
129 const body = raw as Record<string, any>
130 const settings = body.settings
131 if (!settings || typeof settings !== 'object') return null
132
133 // A payload for another brand is a misconfiguration, not content.
134 if (body.slug !== expectedSlug) return null
135
136 const name = text(settings.name, 120)
137 const headline = text(settings.headline, 200)
138 if (name === null || headline === null) return null
139
140 const modules = Array.isArray(settings.modules)
141 ? settings.modules.filter((value: unknown): value is string => typeof value === 'string' && /^[a-z]+$/.test(value))
142 : []
143
144 const offers = Array.isArray(body.offers)
145 ? body.offers.map(parseOffer).filter((offer: CoachingOffer | null): offer is CoachingOffer => offer !== null)
146 : []
147
148 return {
149 slug: expectedSlug,
150 name,
151 headline,
152 description: text(settings.description, 600) ?? '',
153 locale: settings.locale === 'de' ? 'de' : 'en',
154 modules,
155 offers,
156 supportUrl: link(settings.support_url),
157 privacyUrl: link(settings.privacy_url),
158 termsUrl: link(settings.terms_url),
159 }
160}
161
162/** The site renders in en-GB; a German brand prints German money. */
163const LOCALES: Record<'en' | 'de', string> = { en: 'en-GB', de: 'de-DE' }
164
165/** "€19.99 a month", from the coach's own product. This site sets no price. */
166export function formatPrice(product: CoachingProduct, locale: 'en' | 'de' = 'en'): string {
167 const amount = new Intl.NumberFormat(LOCALES[locale], {
168 style: 'currency',
169 currency: product.currency,
170 // Whole prices read better without the trailing zeroes the race uses nowhere else.
171 minimumFractionDigits: product.priceCents % 100 === 0 ? 0 : 2,
172 }).format(product.priceCents / 100)
173
174 const every = product.interval === 'month' ? 'a month' : product.interval === 'year' ? 'a year' : `per ${product.interval}`
175 return `${amount} ${every}`
176}
177
178/** "14 days free first", or nothing at all when the coach set no trial. */
179export function formatTrial(product: CoachingProduct): string {
180 if (product.trialDays <= 0) return ''
181 return product.trialDays === 1 ? '1 day free first' : `${product.trialDays} days free first`
182}
tests/coaching-surface.test.tsmodified+99-0
Changes to tests/coaching-surface.test.ts
@@ -1,5 +1,6 @@
11import { describe, expect, it } from 'bun:test'
22import { coachingModule, coachingModules, enabledModules, indexableModulePaths, modulePath } from '../resources/data/coaching'
3import { formatPrice, formatTrial, parseBrand } from '../resources/functions/coaching-brand'
34
45describe('the coaching module registry', () => {
56 it('uses each HQ module key once, as its own URL segment', () => {
@@ -59,3 +60,101 @@ describe('the coaching module registry', () => {
5960 expect(coachingModule('nope')).toBeUndefined()
6061 })
6162})
63
64/**
65 * The fixture is a verbatim capture of `GET /api/brands/last-soul-ultra` from
66 * the HQ preview stack, preview wording and sample price included. It is here
67 * to pin the shape of the contract, not to describe LSU's real offer.
68 */
69const payload = await Bun.file('tests/fixtures/brand-last-soul-ultra.json').json()
70
71/** A deep copy, so a test that breaks one field cannot leak into the next. */
72function sample(): any {
73 return structuredClone(payload)
74}
75
76describe('reading the published brand', () => {
77 it('reads the real HQ payload', () => {
78 const brand = parseBrand(sample(), 'last-soul-ultra')
79
80 expect(brand?.name).toBe('Last Soul Ultra Preview')
81 expect(brand?.headline).toBe('Train for the next start.')
82 expect(brand?.modules).toContain('calendar')
83 expect(brand?.offers).toHaveLength(1)
84 expect(brand?.offers[0].coach).toBe('LSU Preview Coach')
85 expect(brand?.offers[0].method.version).toBe(1)
86 expect(brand?.offers[0].product.priceCents).toBe(1999)
87 })
88
89 it('refuses a payload published for a different brand', () => {
90 expect(parseBrand(sample(), 'another-race')).toBeNull()
91 })
92
93 it('treats an unpublished brand, a broken payload and an outage alike', () => {
94 for (const raw of [null, undefined, 'nope', 42, {}, { settings: {} }, { slug: 'last-soul-ultra' }])
95 expect(parseBrand(raw, 'last-soul-ultra')).toBeNull()
96 })
97
98 it('drops one malformed offer rather than the whole storefront', () => {
99 const raw = sample()
100 raw.offers.push({ label: 'Broken', method: { name: 'x' } })
101 const brand = parseBrand(raw, 'last-soul-ultra')
102
103 expect(brand?.offers.map((offer: { label: string }) => offer.label)).toEqual(['Preview running'])
104 })
105
106 it('refuses an offer with no price rather than printing a free one', () => {
107 const raw = sample()
108 delete raw.offers[0].product.price_cents
109 expect(parseBrand(raw, 'last-soul-ultra')?.offers).toEqual([])
110 })
111
112 it('keeps a javascript: or relative URL out of an href', () => {
113 const raw = sample()
114 raw.settings.support_url = 'javascript:alert(1)'
115 raw.settings.privacy_url = '/privacy'
116 raw.offers[0].method.contact_url = 'data:text/html,<script>'
117 const brand = parseBrand(raw, 'last-soul-ultra')
118
119 expect(brand?.supportUrl).toBeNull()
120 expect(brand?.privacyUrl).toBeNull()
121 expect(brand?.offers[0].method.contactUrl).toBeNull()
122 expect(brand?.termsUrl).toBe('https://www.lastsoulultra.com/terms')
123 })
124
125 it('ignores a module key that is not a plain lower-case word', () => {
126 const raw = sample()
127 raw.settings.modules = ['plans', '../admin', 42, 'calendar']
128 expect(parseBrand(raw, 'last-soul-ultra')?.modules).toEqual(['plans', 'calendar'])
129 })
130
131 it('falls back to English for a locale HQ does not publish', () => {
132 const raw = sample()
133 raw.settings.locale = 'fr'
134 expect(parseBrand(raw, 'last-soul-ultra')?.locale).toBe('en')
135 })
136})
137
138describe('printing a coach\'s price', () => {
139 const base = { id: 1, name: 'Running', description: '', priceCents: 1999, currency: 'EUR', interval: 'month', trialDays: 0 }
140
141 it('prints the coach\'s own amount and interval', () => {
142 expect(formatPrice(base)).toBe('€19.99 a month')
143 expect(formatPrice({ ...base, priceCents: 12000, interval: 'year' })).toBe('€120 a year')
144 expect(formatPrice({ ...base, currency: 'GBP', priceCents: 2500 })).toBe('£25 a month')
145 })
146
147 it('prints a German brand\'s price the German way', () => {
148 expect(formatPrice(base, 'de')).toContain('19,99')
149 })
150
151 it('names an interval it does not have a phrase for', () => {
152 expect(formatPrice({ ...base, interval: 'week' })).toBe('€19.99 per week')
153 })
154
155 it('says nothing about a trial the coach did not set', () => {
156 expect(formatTrial(base)).toBe('')
157 expect(formatTrial({ ...base, trialDays: 1 })).toBe('1 day free first')
158 expect(formatTrial({ ...base, trialDays: 14 })).toBe('14 days free first')
159 })
160})
tests/fixtures/brand-last-soul-ultra.jsonadded+98-0
Changes to tests/fixtures/brand-last-soul-ultra.json
@@ -0,0 +1,98 @@
1{
2 "slug": "last-soul-ultra",
3 "settings": {
4 "name": "Last Soul Ultra Preview",
5 "locale": "en",
6 "headline": "Train for the next start.",
7 "description": "Test mode. Sample plans and €19.99 monthly test price. No live charges. LSU methods and pricing are not approved.",
8 "logo_url": "http://localhost:3942/assets/lsu/images/lsu-logo.svg",
9 "home_url": "http://localhost:3942/coaching",
10 "support_url": "https://www.lastsoulultra.com/contact",
11 "privacy_url": "https://www.lastsoulultra.com/privacy",
12 "terms_url": "https://www.lastsoulultra.com/terms",
13 "embed_origins": [
14 "http://localhost:3942"
15 ],
16 "theme": {
17 "background": "#000000",
18 "surface": "#0d0d0f",
19 "text": "#ffffff",
20 "muted": "#b6b6bd",
21 "accent": "#dd0f15",
22 "accent_text": "#ffffff",
23 "border": "#333338",
24 "font": "barlow",
25 "heading_font": "anton",
26 "radius": 0,
27 "density": "comfortable"
28 },
29 "modules": [
30 "plans",
31 "calendar",
32 "feedback",
33 "progress",
34 "library",
35 "health",
36 "messages",
37 "account"
38 ],
39 "navigation": [
40 {
41 "module": "plans",
42 "label": "Store",
43 "symbol": "bag"
44 },
45 {
46 "module": "calendar",
47 "label": "Workouts",
48 "symbol": "figure.run"
49 },
50 {
51 "module": "health",
52 "label": "Health"
53 },
54 {
55 "module": "messages",
56 "label": "Messages"
57 },
58 {
59 "module": "account",
60 "label": "Me"
61 }
62 ],
63 "offers": [
64 {
65 "product_id": 1,
66 "method_id": 1,
67 "label": "Preview running"
68 }
69 ],
70 "show_powered_by": false
71 },
72 "app_scheme": "lsupreview",
73 "offers": [
74 {
75 "label": "Preview running",
76 "method": {
77 "id": 1,
78 "name": "LSU preview method",
79 "summary": "Demonstration content, not an approved LSU coach method.",
80 "version": 1,
81 "contact_url": "https://www.lastsoulultra.com/contact"
82 },
83 "coach": {
84 "name": "LSU Preview Coach"
85 },
86 "product": {
87 "id": 1,
88 "slug": "lsu-preview-running",
89 "name": "Preview running subscription",
90 "description": "Stripe TEST MODE. €19.99 is a sample price, not LSU’s approved pricing.",
91 "price_cents": 1999,
92 "currency": "EUR",
93 "billing_interval": "month",
94 "trial_days": 0
95 }
96 }
97 ]
98}