From 73165a0eca9bfa9f8b4c942ee871bfa9548c1464 Mon Sep 17 00:00:00 2001 From: doxa-maps Date: Tue, 15 Sep 2026 11:23:47 +0000 Subject: [PATCH] =?UTF-8?q?feat(qa):=20Start=20QA=20session=20=E2=80=94=20?= =?UTF-8?q?mint=20gateway=20token=20=E2=86=92=20create=20qhoot=20session?= =?UTF-8?q?=20=E2=86=92=20open=20room?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The QA widget's Start now: GET /api/orgs → mint an RS256 gateway token via /api/o//mcp/mint (reads access_token) → POST /sessions to qhoot (Bearer) → open /qhoot/assembly.html?c=. Verified end-to-end (qhoot 201). Backend side (qhoot iss/aud aligned to the mint; mint signing key pinned to certified green-key-1) handled on the app-shell/qhoot infra. --- library/components/qa/QaCorner.vue | 52 +++++++++++- .../cities-internet/cities-internet.js | 28 +++---- public/doxa-maps/cities-internet/index.html | 2 +- .../country-posters/country-posters.js | 82 +++++++++---------- public/doxa-maps/country-posters/index.html | 2 +- public/doxa-maps/demo-map/demo-map.js | 60 +++++++------- public/doxa-maps/demo-map/index.html | 2 +- .../doxa-research-map/doxa-research-map.js | 82 +++++++++---------- public/doxa-maps/doxa-research-map/index.html | 2 +- .../doxa-simple-map/doxa-simple-map.js | 62 +++++++------- public/doxa-maps/doxa-simple-map/index.html | 2 +- .../gospel-resources/gospel-resources.js | 28 +++---- public/doxa-maps/gospel-resources/index.html | 2 +- public/doxa-maps/index.html | 2 +- public/doxa-maps/key-cities/key-cities.js | 24 +++--- public/doxa-maps/manifest.json | 18 ++-- public/doxa-maps/my-upg-100-list/index.html | 2 +- .../my-upg-100-list/my-upg-100-list.js | 34 ++++---- .../doxa-maps/research-dashboard/index.html | 2 +- .../research-dashboard/research-dashboard.js | 50 +++++------ public/doxa-maps/template-bundle/index.html | 2 +- .../template-bundle/template-bundle.js | 26 +++--- 22 files changed, 307 insertions(+), 259 deletions(-) diff --git a/library/components/qa/QaCorner.vue b/library/components/qa/QaCorner.vue index d4ade24dd..341a96b29 100644 --- a/library/components/qa/QaCorner.vue +++ b/library/components/qa/QaCorner.vue @@ -28,6 +28,51 @@ const mapLabel = computed(() => const open = ref(false) function toggle() { open.value = !open.value } +// ── Start a QA session: mint a gateway token → create a qhoot session → open room ── +const starting = ref(false) +const startErr = ref('') +async function startSession() { + starting.value = true; startErr.value = '' + const log = (...a) => { try { console.log('[QaCorner:start]', ...a) } catch (_) {} } + try { + // 1. resolve the org slug (prefer one the user can admin) + const orgsRes = await fetch('/api/orgs', { credentials: 'include' }) + if (!orgsRes.ok) throw new Error('orgs ' + orgsRes.status) + const orgs = (await orgsRes.json())?.orgs || [] + const org = orgs.find(o => (o.roles || []).some(r => /admin|owner|manage/i.test(String(r)))) || orgs[0] + if (!org?.slug) throw new Error('no org membership') + log('org', org.slug) + // 2. mint an RS256 gateway token from the OIDC session + const mintRes = await fetch(`/api/o/${encodeURIComponent(org.slug)}/mcp/mint`, { + method: 'POST', credentials: 'include', + headers: { 'content-type': 'application/json' }, body: JSON.stringify({}) + }) + const mintText = await mintRes.text() + if (!mintRes.ok) throw new Error('mint ' + mintRes.status + ': ' + mintText.slice(0, 160)) + // The mint returns an OAuth-shaped body: the JWT is `access_token` (not `token`). + const token = (() => { try { const j = JSON.parse(mintText); return j.access_token || j.token || '' } catch { return '' } })() + log('minted token?', !!token, 'len', token && token.length) + if (!token) throw new Error('mint returned no token') + // 3. create the qhoot QA session (same-origin /sessions, Bearer) + const crRes = await fetch('/sessions', { + method: 'POST', + headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' }, + body: JSON.stringify({ visibility: 'public', kind: 'qa', question: 'QA on ' + mapLabel.value }) + }) + const crText = await crRes.text() + if (!crRes.ok) throw new Error('create-session ' + crRes.status + ': ' + crText.slice(0, 200)) + const sess = (() => { try { return JSON.parse(crText) } catch { return {} } })() + const code = sess.code || sess.Code || sess.id || sess.ID + log('session created', code) + if (!code) throw new Error('no session code returned') + // 4. open the multiplayer room + window.open('/qhoot/assembly.html?c=' + encodeURIComponent(code), '_blank') + } catch (e) { + startErr.value = String(e?.message || e) + try { console.error('[QaCorner:start] FAILED:', e) } catch (_) {} + } finally { starting.value = false } +} + // ── auth ────────────────────────────────────────────────────────────────── const authState = ref('checking') // checking | in | out | blocked const authUser = ref(null) @@ -108,8 +153,11 @@ useShadowStyles(`