fix: retry Google's transient domain-wide-delegation propagation-delay error
Live Google Workspace delivery validation surfaced a real, self-resolving Google-side condition: freshly-authorized domain-wide delegation can take up to about a minute to propagate across Google's auth infrastructure, producing intermittent unauthorized_client on the OAuth token exchange even with correct configuration. gws-provider.js now classifies unauthorized_client as a temporary (retryable) error instead of permanent, so the existing bounded-backoff worker absorbs it automatically. Also documents the alias-vs-delegated-user env passthrough in docker-compose.dev.yml, and records both real findings (this one and the GOOGLE_DELEGATED_USER alias distinction) plus the full live-delivery validation outcome in the runbook and Phase 5C doc. Validated live: isolated gws-email-test.js send (DKIM/SPF passing, delivered) and a full real lifecycle test (create student, enrollment, real activation email, activate, login, see program, real password-reset email, reset, login with new password) via [email protected].
This commit is contained in:
@@ -71,7 +71,15 @@ async function getAccessToken() {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const e = new Error(`Google OAuth token exchange failed: ${body.error_description || body.error || res.status}`);
|
||||
e.errorClass = res.status >= 500 ? 'temporary' : 'permanent';
|
||||
// 'unauthorized_client' is retried, not treated as permanent — real-world observation
|
||||
// during Phase 5C setup: freshly-authorized domain-wide delegation can take a few minutes
|
||||
// to propagate across Google's auth infrastructure, producing this EXACT error
|
||||
// intermittently (the identical request succeeded, then failed, then failed again, over a
|
||||
// span of about a minute) even though the delegation is correctly configured. A config
|
||||
// that's genuinely wrong still ends up 'failed' once MAX_ATTEMPTS is exhausted — this only
|
||||
// buys the propagation-delay case a real chance to self-resolve instead of giving up on
|
||||
// the very first attempt.
|
||||
e.errorClass = (res.status >= 500 || body.error === 'unauthorized_client') ? 'temporary' : 'permanent';
|
||||
e.errorCode = 'gws_auth_failed';
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,13 @@ services:
|
||||
# network call no matter what recipient it's given.
|
||||
PUBLIC_APP_ORIGIN: ${PUBLIC_APP_ORIGIN:-http://localhost:4200}
|
||||
EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-RootNode Academy}
|
||||
# Empty/unset by default (fail-safe: dev provider, no real email — see email/provider.js).
|
||||
# Set all four in platform/.env only for a deliberate one-off live-delivery test; unset
|
||||
# again afterward so routine dev/test runs never risk a real send.
|
||||
EMAIL_PROVIDER: ${EMAIL_PROVIDER:-}
|
||||
GOOGLE_SERVICE_ACCOUNT_JSON: ${GOOGLE_SERVICE_ACCOUNT_JSON:-}
|
||||
EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-}
|
||||
GOOGLE_DELEGATED_USER: ${GOOGLE_DELEGATED_USER:-}
|
||||
PLATFORM_API_PORT: "4300"
|
||||
# No ADMIN_PASSWORD anymore (Phase 3A) — instructor accounts live in Postgres, created via
|
||||
# `npm run bootstrap-owner` for the first owner and the admin portal after that.
|
||||
|
||||
Reference in New Issue
Block a user