also looking at this
A read-scoped project key, so a Stacks dashboard can read logs without a person's credentials
#15
9 files
+537
-17
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.
| @@ -80,16 +80,28 @@ export async function projectReadAccess(request: any, projectId: string, ownsPro | ||
| 80 | 80 | const presented = bearerFrom(request) |
| 81 | 81 | |
| 82 | 82 | if (looksLikeReadKey(presented)) { |
| 83 | const row = (await db.unsafe( | |
| 83 | // db.unsafe answers an untyped row, so the columns are narrowed here rather | |
| 84 | // than asserted: authorizeRead decides who may read a project and should be | |
| 85 | // reading values it can see the shape of. | |
| 86 | const found = (await db.unsafe( | |
| 84 | 87 | 'SELECT id, read_key, read_key_last_used_at, is_active FROM projects WHERE id = $1 LIMIT 1', |
| 85 | 88 | [String(projectId)], |
| 86 | ))?.[0] ?? null | |
| 89 | ))?.[0] | |
| 90 | const row = found | |
| 91 | ? { | |
| 92 | id: String(found.id), | |
| 93 | read_key: found.read_key == null ? null : String(found.read_key), | |
| 94 | read_key_last_used_at: found.read_key_last_used_at == null ? null : String(found.read_key_last_used_at), | |
| 95 | is_active: found.is_active as boolean | number | null, | |
| 96 | } | |
| 97 | : null | |
| 87 | 98 | const verdict = authorizeRead(row, presented) |
| 88 | 99 | // A key that is valid for some other project must not reveal that this one |
| 89 | 100 | // exists, so its refusal is the same 404 an unknown id gets. |
| 90 | 101 | if (!verdict.ok) |
| 91 | 102 | return verdict.status === 401 ? { ok: false, status: 404, error: 'not found' } : verdict |
| 92 | await recordReadKeyUse(row) | |
| 103 | if (row) | |
| 104 | await recordReadKeyUse(row) | |
| 93 | 105 | return { ok: true, as: 'key' } |
| 94 | 106 | } |
| 95 | 107 | |
| @@ -884,7 +884,7 @@ async function issueReadKey(): Promise<void> { | ||
| 884 | 884 | readKeyOnce.set(String(body.read_key || '')) |
| 885 | 885 | hasReadKey.set(true) |
| 886 | 886 | readKeyUsed.set('') |
| 887 | note(readKeyNote, readKeyNoteOk, 'Copy it now — this is the only time it is shown.', true) | |
| 887 | note(readKeyNote, readKeyNoteOk, 'Copy it now. This is the only time it is shown.', true) | |
| 888 | 888 | } |
| 889 | 889 | catch { |
| 890 | 890 | readKeyBusy.set(false) |
| @@ -1412,7 +1412,7 @@ onMount(() => { | ||
| 1412 | 1412 | <div class="mb-3 font-semibold text-[13px] text-subtle tracking-[0.04em] uppercase">Read key</div> |
| 1413 | 1413 | <div class="px-5 py-5 app-panel"> |
| 1414 | 1414 | <p class="mb-4 text-sm text-subtle"> |
| 1415 | Lets a server read this project's logs without signing in as you — a Stacks dashboard, or anything else reading the API machine-to-machine. | |
| 1415 | Lets a server read this project's logs without signing in as you: a Stacks dashboard, or anything else reading the API machine-to-machine. | |
| 1416 | 1416 | Unlike the API key above this one <em>is</em> a secret: it reads your logs, it belongs to no account, and it keeps working after you leave. |
| 1417 | 1417 | Send it as <span class="font-mono">Authorization: Bearer</span>. |
| 1418 | 1418 | </p> |
| @@ -1431,7 +1431,7 @@ onMount(() => { | ||
| 1431 | 1431 | {{-- Otherwise: whether one exists, and whether it is still in use. --}} |
| 1432 | 1432 | <div :show="!readKeyOnce()"> |
| 1433 | 1433 | <div class="flex flex-wrap gap-2 items-center"> |
| 1434 | <span class="flex-1 min-w-[220px] text-sm" :class="hasReadKey() ? 'text-ink' : 'text-subtle'" :text="hasReadKey() ? `A read key is active — ${readKeyUsedLabel()}.` : 'No read key. Issue one to let a server read this project.'"></span> | |
| 1434 | <span class="flex-1 min-w-[220px] text-sm" :class="hasReadKey() ? 'text-ink' : 'text-subtle'" :text="hasReadKey() ? `A read key is active, ${readKeyUsedLabel()}.` : 'No read key. Issue one to let a server read this project.'"></span> | |
| 1435 | 1435 | <Button variant="ghost" @click="issueReadKey" :loading="readKeyBusy()" :label="hasReadKey() ? 'Replace' : 'Issue key'" className="btn-quiet btn-sm">Issue key</Button> |
| 1436 | 1436 | <span :show="hasReadKey()"> |
| 1437 | 1437 | <Button variant="ghost" @click="revokeReadKey" :loading="readKeyBusy()" className="btn-danger btn-sm">Revoke</Button> |