also looking at this
A read-scoped site key, so a Stacks dashboard can read analytics without a person's credentials
#68
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.
| @@ -1445,11 +1445,11 @@ function readKeyStatus(): string { | ||
| 1445 | 1445 | return 'No read key. Issue one to let a server read this site.' |
| 1446 | 1446 | const at = readKeyUsed() |
| 1447 | 1447 | if (!at) |
| 1448 | return 'A read key is active — never used.' | |
| 1448 | return 'A read key is active, never used.' | |
| 1449 | 1449 | const when = new Date(at) |
| 1450 | 1450 | 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()}.` | |
| 1453 | 1453 | } |
| 1454 | 1454 | |
| 1455 | 1455 | function copyReadKey() { |
| @@ -826,8 +826,10 @@ function authUserId(request: HandlerRequest): string | null { | ||
| 826 | 826 | * still answers 401 when no credential resolves. |
| 827 | 827 | */ |
| 828 | 828 | async 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, '') | |
| 831 | 833 | if (!looksLikeReadKey(presented)) |
| 832 | 834 | return requireSiteRole(request, siteId, 'viewer') |
| 833 | 835 | |
| @@ -71,7 +71,12 @@ describe('authorizeRead', () => { | ||
| 71 | 71 | // against a caller presenting nothing. |
| 72 | 72 | for (const presented of [KEY, null, undefined, '']) |
| 73 | 73 | 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) | |
| 75 | 80 | }) |
| 76 | 81 | |
| 77 | 82 | test('refuses another project\'s key with 401', () => { |