ReviewOS

also looking at this

stacks/analyticshq

A read-scoped site key, so a Stacks dashboard can read analytics without a person's credentials

#68
Merged glennmichael123 wants to merge feat/read-scoped-site-key into main
7 files +550 -11

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/views/dashboard.stxmodified+3-3
Changes to resources/views/dashboard.stx
@@ -1445,11 +1445,11 @@ function readKeyStatus(): string {
14451445 return 'No read key. Issue one to let a server read this site.'
14461446 const at = readKeyUsed()
14471447 if (!at)
1448 return 'A read key is active — never used.'
1448 return 'A read key is active, never used.'
14491449 const when = new Date(at)
14501450 return Number.isNaN(when.getTime())
1451 ? 'A read key is active — used recently.'
1452 : `A read key is active — last used ${when.toLocaleString()}.`
1451 ? 'A read key is active, used recently.'
1452 : `A read key is active, last used ${when.toLocaleString()}.`
14531453}
14541454
14551455function copyReadKey() {
routes/analytics.tsmodified+4-2
Changes to routes/analytics.ts
@@ -826,8 +826,10 @@ function authUserId(request: HandlerRequest): string | null {
826826 * still answers 401 when no credential resolves.
827827 */
828828async function requireSiteRead(request: HandlerRequest, siteId: string): Promise<Response | null> {
829 const authHeader = request.headers?.get?.('authorization') ?? ''
830 const presented = request.bearerToken?.() ?? authHeader.replace(/^Bearer\s+/i, '')
829 // Read off the header rather than a bearerToken() helper: HandlerRequest
830 // declares no such method here, and line 3272 reads the same header the same
831 // way, so this stays consistent with the rest of the file.
832 const presented = String(request.headers?.get?.('authorization') ?? '').replace(/^Bearer\s+/i, '')
831833 if (!looksLikeReadKey(presented))
832834 return requireSiteRole(request, siteId, 'viewer')
833835
tests/unit/read-key.test.tsmodified+6-1
Changes to tests/unit/read-key.test.ts
@@ -71,7 +71,12 @@ describe('authorizeRead', () => {
7171 // against a caller presenting nothing.
7272 for (const presented of [KEY, null, undefined, ''])
7373 expect(authorizeRead({ ...site, read_key: null }, presented).ok).toBe(false)
74 expect(authorizeRead({ ...site, read_key: null }, KEY).status).toBe(403)
74 // Narrowed rather than reached into: the refusal shape only carries a
75 // status on the failing branch of the union.
76 const refused = authorizeRead({ ...site, read_key: null }, KEY)
77 expect(refused.ok).toBe(false)
78 if (!refused.ok)
79 expect(refused.status).toBe(403)
7580 })
7681
7782 test('refuses another project\'s key with 401', () => {