Index: src/config/google.js
===================================================================
--- src/config/google.js	(revision 32cc601dd3bba9d9131f32ae282712f54feb838c)
+++ src/config/google.js	(revision 9e27d64c70dff768f7a07cb5f10586ad65f56c42)
@@ -1,75 +1,60 @@
-// Google-login via de centrale Klonkt-broker (license.klonkt.com).
+// Google OAuth2 voor LUISTERAARS (reageren). Per-instance: de self-hoster zet
+// z'n EIGEN Google-client in .env. Zo hangt elke site aan z'n eigen Google Cloud
+// project — geen centrale afhankelijkheid, geen gedeelde aansprakelijkheid.
 //
-// Deze instance praat NOOIT zelf met Google. De broker doet de OAuth-dans met één
-// centrale Google-client en stuurt een kortlevend, gesigneerd identity-token terug;
-// dat verifiëren we offline tegen de broker-pubkey. Zo hoeft geen enkele self-host
-// een eigen Google-client aan te maken.
+// Optioneel: staat dit niet ingesteld, dan is er simpelweg geen "Login met
+// Google"-knop en blijft de rest van de site werken. Google-login geeft NOOIT
+// beheerrechten — beheer gaat via het wachtwoord-account.
 //
 // Config via env:
-//   KLONKT_BROKER_URL = https://license.klonkt.com
-//   SITE_ORIGIN       = het eigen publieke origin (bv https://roboburr.com) —
-//                       bepaalt de callback + de audience die we eisen.
-//   ADMIN_EMAIL       = de Google-mail die owner/admin (god) is op deze instance.
+//   GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
+//   GOOGLE_REDIRECT_URI = https://<dit-domein>/auth/google/callback
 
-import { importSPKI, jwtVerify } from 'jose';
+const CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
+const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
+const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || '';
 
-const ALG = 'EdDSA';
-const ISSUER = 'klonkt-license';
+const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
+const TOKEN_URL = 'https://oauth2.googleapis.com/token';
+const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';
 
-const BROKER_URL = (process.env.KLONKT_BROKER_URL || '').replace(/\/$/, '');
-const SITE_ORIGIN = (process.env.SITE_ORIGIN || '').replace(/\/$/, '');
-
-export function brokerConfigured() {
-  return !!(BROKER_URL && SITE_ORIGIN);
+export function googleConfigured() {
+  return !!(CLIENT_ID && CLIENT_SECRET && REDIRECT_URI);
 }
 
-// Waar de broker naartoe terugstuurt (moet in de broker-allowlist staan).
-export function callbackUrl() {
-  return `${SITE_ORIGIN}/auth/google/callback`;
+export function authorizeUrl(state) {
+  const p = new URLSearchParams({
+    client_id: CLIENT_ID,
+    redirect_uri: REDIRECT_URI,
+    response_type: 'code',
+    scope: 'openid email profile',
+    state,
+    access_type: 'online',
+    prompt: 'select_account',
+  });
+  return `${AUTH_URL}?${p.toString()}`;
 }
 
-export function brokerStartUrl(istate) {
-  const p = new URLSearchParams({ return: callbackUrl(), istate });
-  return `${BROKER_URL}/auth/google/start?${p.toString()}`;
+export async function exchangeCode(code) {
+  const body = new URLSearchParams({
+    code,
+    client_id: CLIENT_ID,
+    client_secret: CLIENT_SECRET,
+    redirect_uri: REDIRECT_URI,
+    grant_type: 'authorization_code',
+  });
+  const r = await fetch(TOKEN_URL, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+    body,
+  });
+  if (!r.ok) throw new Error(`Google token-exchange faalde: ${r.status}`);
+  return r.json(); // { access_token, id_token, ... }
 }
 
-// Broker-pubkey ophalen + cachen (bij fout NIET permanent cachen).
-let _pubkeyPromise = null;
-function getPublicKey() {
-  if (!_pubkeyPromise) {
-    _pubkeyPromise = (async () => {
-      const r = await fetch(`${BROKER_URL}/pubkey`);
-      if (!r.ok) throw new Error(`broker /pubkey faalde: ${r.status}`);
-      return importSPKI(await r.text(), ALG);
-    })().catch((e) => {
-      _pubkeyPromise = null;
-      throw e;
-    });
-  }
-  return _pubkeyPromise;
+// Returns { sub, email, email_verified, name, picture }.
+export async function fetchUserinfo(accessToken) {
+  const r = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}` } });
+  if (!r.ok) throw new Error(`Google userinfo faalde: ${r.status}`);
+  return r.json();
 }
-
-// Verifieer het identity-token van de broker. Returnt de payload
-// { typ:'identity', sub, email, name, picture, jti, exp, ... }.
-export async function verifyIdentityToken(token) {
-  const key = await getPublicKey();
-  const { payload } = await jwtVerify(token, key, {
-    issuer: ISSUER,
-    algorithms: [ALG],
-    audience: SITE_ORIGIN, // token moet voor ÓNZE site bedoeld zijn
-  });
-  if (payload.typ !== 'identity') throw new Error('verkeerd tokentype');
-  return payload;
-}
-
-// Kleine in-memory jti-cache tegen replay binnen de (korte) geldigheidsduur.
-// Returnt false als de jti al gebruikt is.
-const _usedJti = new Map(); // jti -> exp (epoch seconds)
-export function consumeJti(jti, expEpoch) {
-  if (!jti) return true; // geen jti = niets te dedupen
-  const now = Math.floor(Date.now() / 1000);
-  for (const [k, e] of _usedJti) if (e < now) _usedJti.delete(k);
-  if (_usedJti.has(jti)) return false;
-  _usedJti.set(jti, expEpoch || now + 600);
-  return true;
-}
