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.

config/server.tsmodified+28-0
Changes to config/server.ts
@@ -1,4 +1,11 @@
11import type { ServerConfig } from '@stacksjs/types'
2import coaching from './coaching'
3
4/**
5 * The HQ origin the coaching panel is framed from. Permissions-Policy needs
6 * the exact origin, quoted, and it must agree with what CoachingPortal frames.
7 */
8const hqOrigin = new URL(coaching.origin).origin
29
310/**
411 * **Server Configuration**
@@ -10,6 +17,27 @@ import type { ServerConfig } from '@stacksjs/types'
1017 * Discord or GitHub Discussions.
1118 */
1219export default {
20 /**
21 * **Response headers**
22 *
23 * `path` or `path/*` to the headers that path answers with.
24 *
25 * Recording a voice message is the whole point of the coaching feedback
26 * module, and a cross-origin frame can only reach the microphone if the
27 * page embedding it delegates that permission. Without this the browser
28 * blocks `getUserMedia` inside HQ's frame and the feature fails quietly,
29 * which is the worst way for it to fail: nothing errors, the button simply
30 * never does anything.
31 *
32 * Only the two routes that need it. The hub embeds every section, including
33 * feedback; the seven other module routes pin one section each and have no
34 * use for a microphone.
35 */
36 headers: {
37 '/coaching': { 'Permissions-Policy': `microphone=(self "${hqOrigin}")` },
38 '/coaching/feedback': { 'Permissions-Policy': `microphone=(self "${hqOrigin}")` },
39 },
40
1341 /**
1442 * **API Proxy**
1543 *
scripts/serve-preview.tsmodified+19-1
Changes to scripts/serve-preview.ts
@@ -1,6 +1,23 @@
11/* eslint-disable no-console */
22import { resolve, sep } from 'node:path'
33import { compressResponse } from '@stacksjs/stx/compression'
4import server from '../config/server'
5
6/**
7 * The same per-path headers the production server applies from
8 * config/server.ts, matched the same way: `path/*` is a prefix, anything
9 * else is exact. A preview that grants the microphone where production does
10 * not is a preview that cannot find the bug, so this reads the one rule set
11 * rather than keeping a second hard-coded origin beside it.
12 */
13function configuredHeaders(pathname: string): Record<string, string> {
14 const matched: Record<string, string> = {}
15 for (const [pattern, headers] of Object.entries(server.headers ?? {})) {
16 const hit = pattern.endsWith('/*') ? pathname.startsWith(pattern.slice(0, -1)) : pathname === pattern
17 if (hit) Object.assign(matched, headers)
18 }
19 return matched
20}
421
522const root = resolve('dist')
623Bun.serve({ hostname: '127.0.0.1', port: 3942, async fetch(request) {
@@ -15,6 +32,7 @@ Bun.serve({ hostname: '127.0.0.1', port: 3942, async fetch(request) {
1532 let file = Bun.file(path)
1633 let status = 200
1734 if (!await file.exists()) { file = Bun.file(resolve(root, '404.html')); status = 404 }
18 return compressResponse(request, new Response(file, { status, headers: { 'Cache-Control': 'no-store', 'Permissions-Policy': 'microphone=(self "http://localhost:3940")' } }))
35 const headers = { 'Cache-Control': 'no-store', ...configuredHeaders(pathname.replace(/(.)\/$/, '$1')) }
36 return compressResponse(request, new Response(file, { status, headers }))
1937} })
2038console.log('LSU preview listening at http://localhost:3942')
tests/coaching-surface.test.tsmodified+31-0
Changes to tests/coaching-surface.test.ts
@@ -1,6 +1,8 @@
11import { describe, expect, it } from 'bun:test'
22import { coachingModule, coachingModules, enabledModules, indexableModulePaths, modulePath } from '../resources/data/coaching'
33import { formatPrice, formatTrial, parseBrand } from '../resources/functions/coaching-brand'
4import coaching from '../config/coaching'
5import server from '../config/server'
46import { coachingBrand, coachingOrigin, coachingSection, portalElementUrl, portalUrl } from '../resources/functions/coaching-source'
57
68describe('the coaching module registry', () => {
@@ -195,3 +197,32 @@ describe('where the portal is', () => {
195197 expect(portalElementUrl('https://hq.training')).toBe('https://hq.training/assets/elements/hq-coaching.js')
196198 })
197199})
200
201describe('the microphone the feedback module needs', () => {
202 /** Exactly how @stacksjs/server matches config.server.headers. */
203 function headersFor(pathname: string): Record<string, string> {
204 const matched: Record<string, string> = {}
205 for (const [pattern, headers] of Object.entries(server.headers ?? {})) {
206 const hit = pattern.endsWith('/*') ? pathname.startsWith(pattern.slice(0, -1)) : pathname === pattern
207 if (hit) Object.assign(matched, headers)
208 }
209 return matched
210 }
211
212 it('is delegated on the two routes that frame it', () => {
213 for (const path of ['/coaching', '/coaching/feedback']) {
214 expect(headersFor(path)['Permissions-Policy']).toBe(`microphone=(self "${new URL(coaching.origin).origin}")`)
215 }
216 })
217
218 it('names the origin the panel is actually framed from', () => {
219 // A policy naming a different host than the iframe src grants nothing.
220 const policy = headersFor('/coaching/feedback')['Permissions-Policy'] ?? ''
221 expect(policy).toContain(new URL(portalUrl('feedback')).origin)
222 })
223
224 it('is not handed to a route with no microphone in it', () => {
225 for (const path of ['/', '/format', '/coaching/calendar', '/coaching/account'])
226 expect(headersFor(path)['Permissions-Policy']).toBeUndefined()
227 })
228})