Skip to content

feat(calendar): per-provider credentials and settings namespace (#1394) - #1424

Draft
h4yfans wants to merge 1 commit into
h4yfans/calendar-sync-engine-generalizationfrom
h4yfans/calendar-provider-credentials-settings
Draft

feat(calendar): per-provider credentials and settings namespace (#1394)#1424
h4yfans wants to merge 1 commit into
h4yfans/calendar-sync-engine-generalizationfrom
h4yfans/calendar-provider-credentials-settings

Conversation

@h4yfans

@h4yfans h4yfans commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #1394. Phase 0 of the multi-provider calendar epic #1390.

Stacked on #1423 (#1393) → #1422 (#1392) → #1407 (#1391). Base is h4yfans/calendar-sync-engine-generalization.

What

Two constants were doing the nailing: SERVICE = 'com.memry.calendar.google' in google/keychain.ts, and the literal group key 'calendar.google'.

Keychain

calendar/provider/credentials.ts derives the service name per provider — com.memry.calendar.<providerId>. For google that produces the exact string the constant held, so every credential already on disk still resolves. The account-key scheme (${kind}-${accountId} plus the MEMRY_DEVICE dev-profile suffix) and the LEGACY_DEFAULT_ACCOUNT_ID path are preserved verbatim.

GoogleTokenKindProviderSecretKind = 'access-token' | 'refresh-token' | 'password'. CalDAV needs password for basic auth; ICS has no secret at all — its URL is the credential and lives in calendar_sources.metadata.

Settings

calendar.<providerId> groups in a shared neutral shape (CalendarProviderSettings). Google's group is frozen exactly as it is — same key, same keys, pushEventsToGoogle and all — and readCalendarProviderSettings translates that one historic name to pushEventsToProvider on the way out, so the sync engine only ever sees the neutral form. There is no migration and no new key is written into the Google row; a test asserts the stored JSON keeps precisely its original key set.

Agent read consent — the decision

agentReadEventsConsent is promoted from a Google special case to the base schema, with per-provider values:

No provider's external events are readable by the AI agent until that provider has been explicitly consented to. null — never asked — reads as no, exactly like a refusal.

Google Workspace Limited Use is what forced the question first; the answer became the house rule, and Google's current behavior is now an instance of it rather than an exception to it. This is the "privacy is the product" reading and still satisfies the Google obligation.

The renderer gate follows. calendar.getRange resolves includeExternal by requiring consent for every registered provider, because the call carries one boolean rather than a per-provider list — so one unconsented provider keeps every external event out rather than risking one leaking through. That trade-off is stated plainly in the docs; a per-provider filter is a natural follow-up once a second provider actually ships.

Documented in apps/docs/src/user-guide/calendar.md (new Connected Calendars and AI Features section, with the Google specifics kept underneath) and apps/docs/src/user-guide/ai/agent-mcp.md.

Verification

  • pnpm typecheck — 16/16 green
  • pnpm lint — 0 errors
  • pnpm ipc:check, pnpm check:contracts — green
  • pnpm docs:impact --base origin/main --strict — green (docs changed on this branch)
  • pnpm docs:build — green
  • @memry/contracts — 50 files, 1655 tests
  • Desktop: main 6378 passed / 1 expected fail / 4 skipped (519 files) · renderer 6965 passed / 6 skipped · shared 2455

New tests:

  • provider/settings.test.ts — a calendar.google fixture written by the Google-only build parses unchanged (including one written before agentReadEventsConsent existed, which must land on "not asked" rather than fail); it reads back correctly through the neutral accessor; a write keeps the legacy key and adds none; a new provider's group never touches Google's; every provider starts at "agent may not read"; a corrupt group falls back to defaults rather than to permissive values.
  • provider/credentials.test.ts — google's service name is byte-identical to the old constant; the account-key scheme and dev-profile suffix are unchanged; a credential written by a previous Google-only build resolves through both the neutral reader and the Google accessor; the LEGACY_DEFAULT_ACCOUNT_ID path resolves; two providers sharing one account id are partitioned, and disconnecting CalDAV does not log the user out of Google.

One existing renderer test file (agent-mcp/desktop-api-handler.test.tsx) had its mocks repointed from getCalendarGoogleSettings to getCalendarProviderSettings + listProviders, matching the gate's new source. Its assertions are unchanged, and the excluded-operations list gained the neutral twins alongside the Google-named ones.

@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request test labels Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit ccd6a4b.

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.61905% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../desktop/src/main/calendar/provider/credentials.ts 94.11% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@h4yfans
h4yfans force-pushed the h4yfans/calendar-sync-engine-generalization branch from 302f054 to ec745e7 Compare August 13, 2026 16:17
@h4yfans
h4yfans force-pushed the h4yfans/calendar-provider-credentials-settings branch 2 times, most recently from f7aeaa4 to 57ac489 Compare August 13, 2026 16:19
Credentials and settings were both nailed to Google by a constant:
`SERVICE = 'com.memry.calendar.google'` and the literal settings group key
`'calendar.google'`.

**Keychain.** `calendar/provider/credentials.ts` derives the service name per
provider — `com.memry.calendar.<providerId>` — which for google produces the
exact string the constant held, so every credential already on disk still
resolves. The account-key scheme (`${kind}-${accountId}` plus the dev-profile
suffix) and the `LEGACY_DEFAULT_ACCOUNT_ID` path are preserved verbatim.
`GoogleTokenKind` becomes `ProviderSecretKind = 'access-token' |
'refresh-token' | 'password'`; CalDAV needs the password, ICS has no secret at
all.

**Settings.** `calendar.<providerId>` groups in a shared neutral shape
(`CalendarProviderSettings`). Google's group is frozen exactly as it is —
same key, same keys, `pushEventsToGoogle` included — and
`readCalendarProviderSettings` translates that one historic name to
`pushEventsToProvider` so the sync engine only ever sees the neutral form.
No migration; a new provider only ever writes a new key.

**Agent read consent — decision.** `agentReadEventsConsent` is promoted from a
Google special case to the base schema, with per-provider values. No provider's
external events are readable by the agent until that provider is explicitly
consented to; null (never asked) reads as no. Google Workspace Limited Use
forced the question first, and Google's behavior is now an instance of the rule
rather than an exception to it.

The renderer gate follows: `calendar.getRange` resolves `includeExternal` by
requiring consent for every registered provider, since the call carries one
flag rather than a per-provider list. One unconsented provider keeps every
external event out rather than risking one leaking through.

Documented under `apps/docs/src/user-guide/calendar.md` and
`apps/docs/src/user-guide/ai/agent-mcp.md`.
@h4yfans
h4yfans force-pushed the h4yfans/calendar-provider-credentials-settings branch from 57ac489 to ccd6a4b Compare August 13, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant