Skip to content

Tell the host to enable the calendar API, rather than showing an empty picker - #6

Merged
sergebulaev merged 1 commit into
CCCrafts:mainfrom
dmno-dev:fix/calendar-api-not-enabled
Sep 15, 2026
Merged

sergebulaev merged 1 commit into
CCCrafts:mainfrom
dmno-dev:fix/calendar-api-not-enabled

Conversation

@theoephraim

@theoephraim theoephraim commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

I was setting things up an forgot to enable the google calendar API. This resulted in some awkward stuff in the UI rather than a clear error. Feel free to toss / reimplement.


What happens today

Creating OAuth credentials in Google Cloud does not enable the Calendar API. So the very first calendar a fresh self-hoster connects returns:

403 { "error": { "errors": [{ "reason": "accessNotConfigured" }],
      "message": "Google Calendar API has not been used in project N before or it is disabled..." } }

expectOk only treats a 403 as reconnect-required when the body matches insufficientPermissions|insufficient_scope|ErrorAccessDenied. accessNotConfigured matches none of those, so it became a generic CalendarApiError — and both listCalendars callers discarded it:

  • the connect callback's catch {}, which kept the grant (correctly) but dropped the reason
  • listCalendarsSafely, which returned []

The host ends up with a saved connection, an empty calendar picker, and nothing in the product or the log explaining it. I hit this on a fresh deploy and, having no other signal, reconnected twice — which cannot work, because the grant was valid the whole time. An empty picker is also indistinguishable from an account that genuinely has no calendars.

It's worth noting this is a first-run failure rather than an exotic one: every new deployment hits it unless the operator already knew to enable the API.

What this changes

  • CalendarSetupRequiredError, deliberately separate from CalendarReconnectRequiredError. The two need opposite advice, and telling someone to reconnect a perfectly valid grant sends them in a loop they have no way to diagnose. It carries howToFix — the console URL — because the fix is neither guessable from the status code nor performable inside Punctual.
  • isApiNotEnabled(body), pure and unit-tested. Google reports the same condition three ways (accessNotConfigured, SERVICE_DISABLED, or prose naming the project) depending on which surface answers. It deliberately does not match a bare is disabled, which shows up in unrelated 403s — a false positive here would send a host to change a console setting that was never the problem.
  • Checked before the existing scope branch, as the more specific reading of a 403. A genuine scope failure still classifies as needs-reconnect; there's a test pinning that.
  • Both call sites log the reason instead of swallowing it. The grant is still preserved on any failure, exactly as before.
  • The connections page renders it and offers no Reconnect button, since that's the one action known not to work.

SETUP_HINTS covers Microsoft too, though accessNotConfigured is Google-specific in practice — Entra's equivalent is an unconsented application permission.

Tests

6 new, all in test/core (pure, no Workers runtime):

  • surfaces as needs-setup, not needs-reconnect
  • carries a fix the host can act on
  • classifies the three shapes Google reports it in, and nothing else
  • leaves a genuine scope failure classified as needs-reconnect
  • the page explains it rather than showing an empty picker
  • the page says nothing extra when the picker is simply empty and the cause is unknown

npm run typecheck clean, npm test 880 passing locally.

Not included

The same deploy surfaced a second bug I've left out to keep this focused, and I'm happy to send it separately if useful: calendar_connections has no unique constraint on (user_id, provider) and the connect handler always inserts, so the reconnect loop above silently created two Google connections for one account. They can't be told apart either, since provider_account_email is ''GOOGLE_CALENDAR_SCOPES omits openid/email, so Google returns no id_token for emailFromIdToken to read. Adding those two non-sensitive scopes and upserting on (user_id, provider, provider_account_email) would fix it without blocking a genuine work+personal setup.

…y picker

Creating OAuth credentials does not enable the Google Calendar API, so the
first calendar a self-hoster connects returns 403 accessNotConfigured. On a
403 `expectOk` matched only insufficientPermissions/insufficient_scope, so
this fell through to a generic CalendarApiError — and both listCalendars
callers discarded it entirely. The result was a saved connection, an empty
calendar picker, and nothing in the product or the log saying why.

The obvious response to that is to reconnect, which cannot help: the grant is
valid and the API is simply off. Reconnecting lands the host back on the same
empty picker, so the failure reads as a bug in the product rather than a
setting in their own console.

Classify it as CalendarSetupRequiredError, deliberately distinct from
CalendarReconnectRequiredError because the two need opposite advice, and put
the console URL on the error itself — the fix is neither guessable from the
status code nor performable inside Punctual. The connections page renders it
and offers no Reconnect button, since that is the one action known not to
work. Both call sites now log the reason instead of swallowing it; the grant
is still kept on any failure, as before.
@sergebulaev
sergebulaev merged commit 53fc2f9 into CCCrafts:main Sep 15, 2026
@sergebulaev

Copy link
Copy Markdown
Contributor

Merged — thank you. This is exactly the kind of first-run failure that never shows up on a developer's own machine, and the separation from needs-reconnect is the right call. One follow-up on main: the Microsoft hint now says delegated permission rather than application permission, since that is what this app requests. The (user_id, provider) duplicate-connection issue you mention in "Not included" is real and welcome as a separate PR if you feel like it; otherwise it is on our list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants