diff --git a/apps/desktop/src/main/calendar/provider/adapter.ts b/apps/desktop/src/main/calendar/provider/adapter.ts new file mode 100644 index 000000000..6680c66e8 --- /dev/null +++ b/apps/desktop/src/main/calendar/provider/adapter.ts @@ -0,0 +1,142 @@ +import type { + CalendarAttendee, + CalendarConferenceData, + CalendarReminders, + CalendarVisibility +} from '@memry/db-schema/schema/calendar-events' +import type { CalendarSyncSourceType } from '../types' + +/** + * How a provider tells us "what changed since last time". + * + * - `sync-token` — Google: opaque token returned with each page + * - `delta-link` — Microsoft Graph: `@odata.deltaLink` + * - `sync-collection` — CalDAV RFC 6578 + * - `ctag-etag` — CalDAV without RFC 6578: collection ctag, then per-item etags + * - `conditional-get` — plain HTTP `ETag` / `Last-Modified` on a whole feed (ICS) + * - `full` — no incremental support; every pass re-reads everything + */ +export type ProviderIncrementalMode = + 'sync-token' | 'delta-link' | 'sync-collection' | 'ctag-etag' | 'conditional-get' | 'full' + +/** Which connect flow the UI shell has to render for this provider. */ +export type ProviderAuthFlow = 'oauth2' | 'basic' | 'url' | 'none' + +export interface ProviderCapabilities { + /** False for read-only providers (ICS). The engine — not the adapter — refuses writes. */ + supportsWrite: boolean + /** Can we provision our own "memrynote" calendar on the remote? */ + supportsCreateCalendar: boolean + /** Real-time change notifications. False means the runner polls. */ + supportsPush: boolean + /** More than one connected account per provider. */ + supportsMultiAccount: boolean + incrementalMode: ProviderIncrementalMode + authFlow: ProviderAuthFlow +} + +export interface RemoteCalendarDescriptor { + id: string + title: string + timezone: string | null + color: string | null + isPrimary: boolean +} + +/** + * One event as the provider reports it. Field names are carried over verbatim + * from the Google-era shape so the mappers and the `calendar_external_events` + * mirror keep working untouched; `raw` stays the provider's own payload. + */ +export interface RemoteCalendarEvent { + id: string + calendarId: string + title: string + description: string | null + location: string | null + startAt: string + endAt: string | null + isAllDay: boolean + timezone: string + status: 'confirmed' | 'tentative' | 'cancelled' + etag: string | null + updatedAt: string | null + attendees: CalendarAttendee[] | null + reminders: CalendarReminders | null + visibility: CalendarVisibility | null + colorId: string | null + conferenceData: CalendarConferenceData | null + recurringEventId: string | null + originalStartTime: string | null + raw: Record +} + +export interface UpsertRemoteEventInput { + sourceType: CalendarSyncSourceType + sourceId: string + title: string + description: string | null + location: string | null + startAt: string + endAt: string | null + isAllDay: boolean + timezone: string + recurrence: string[] | null + attendees?: CalendarAttendee[] | null + reminders?: CalendarReminders | null + visibility?: CalendarVisibility | null + colorId?: string | null + conferenceData?: CalendarConferenceData | null + recurringEventId?: string | null + originalStartTime?: string | null +} + +export interface ListRemoteEventsInput { + calendarId: string + /** Whatever the provider's `incrementalMode` calls a cursor, opaque to us. */ + syncCursor?: string | null + timeMin?: string | null + timeMax?: string | null +} + +export interface ListRemoteEventsResult { + events: RemoteCalendarEvent[] + nextSyncCursor: string | null +} + +export interface WatchCalendarInput { + calendarId: string + channelId: string + token: string + webhookUrl: string + ttlSeconds: number +} + +export interface WatchCalendarResult { + resourceId: string + expiration: number +} + +/** + * The one surface the calendar sync engine is allowed to talk to. + * + * Optional members map one-to-one onto `ProviderCapabilities`: a provider with + * `supportsWrite: false` omits `upsertEvent`/`deleteEvent`, `supportsPush: + * false` omits `watch`/`unwatch`, `supportsCreateCalendar: false` omits + * `createCalendar`. + */ +export interface CalendarProviderAdapter { + listCalendars(): Promise + createCalendar?(input: { title: string; timezone: string }): Promise + listEvents(input: ListRemoteEventsInput): Promise + getEvent(input: { calendarId: string; eventId: string }): Promise + upsertEvent?(input: { + calendarId: string + eventId: string | null + event: UpsertRemoteEventInput + ifMatch?: string | null + }): Promise + deleteEvent?(input: { calendarId: string; eventId: string }): Promise + watch?(input: WatchCalendarInput): Promise + unwatch?(input: { channelId: string; resourceId: string }): Promise +} diff --git a/apps/desktop/src/main/calendar/provider/errors.test.ts b/apps/desktop/src/main/calendar/provider/errors.test.ts new file mode 100644 index 000000000..16b9aa91c --- /dev/null +++ b/apps/desktop/src/main/calendar/provider/errors.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' +import { + ProviderAuthError, + ProviderConflictError, + ProviderError, + ProviderGoneError, + ProviderRateLimitError, + ProviderTransientError +} from './errors' + +describe('calendar provider error taxonomy', () => { + it('every provider error is an Error, a ProviderError, and reports its own class name', () => { + const cases = [ + new ProviderAuthError('reconnect'), + new ProviderGoneError('cursor dead'), + new ProviderConflictError('etag mismatch'), + new ProviderRateLimitError('slow down'), + new ProviderTransientError('network blip') + ] + + for (const error of cases) { + expect(error).toBeInstanceOf(Error) + expect(error).toBeInstanceOf(ProviderError) + expect(error.name).toBe(error.constructor.name) + } + }) + + it('discriminates by class so the engine can branch on the condition', () => { + const gone: ProviderError = new ProviderGoneError('cursor dead') + + expect(gone).toBeInstanceOf(ProviderGoneError) + expect(gone).not.toBeInstanceOf(ProviderAuthError) + expect(gone).not.toBeInstanceOf(ProviderConflictError) + }) + + it('carries the provider id and the original cause when the adapter supplies them', () => { + const cause = new Error('HTTP 410') + const error = new ProviderGoneError('cursor dead', { providerId: 'google', cause }) + + expect(error.providerId).toBe('google') + expect(error.cause).toBe(cause) + }) + + it('leaves providerId and cause undefined when the adapter omits them', () => { + const error = new ProviderAuthError('reconnect') + + expect(error.providerId).toBeUndefined() + expect(error.cause).toBeUndefined() + }) + + it('keeps the rate-limit retry hint, and reads null when the provider gave none', () => { + expect(new ProviderRateLimitError('slow down', { retryAfterMs: 30_000 }).retryAfterMs).toBe( + 30_000 + ) + expect(new ProviderRateLimitError('slow down').retryAfterMs).toBeNull() + }) +}) diff --git a/apps/desktop/src/main/calendar/provider/errors.ts b/apps/desktop/src/main/calendar/provider/errors.ts new file mode 100644 index 000000000..b9f03fc1e --- /dev/null +++ b/apps/desktop/src/main/calendar/provider/errors.ts @@ -0,0 +1,54 @@ +/** + * Provider-neutral calendar error taxonomy. + * + * Today the sync engine sniffs Google's wire shape directly (`status === 410` + * for an invalidated syncToken, `status === 412` for an etag mismatch). Every + * other protocol we are about to speak reports the same conditions differently + * — CalDAV answers 412 on a stale ETag but 404/`valid-sync-token` on a dead + * sync-collection token, Microsoft Graph hands back `resyncRequired` in a JSON + * body. Adapters translate their own wire errors into these classes so the + * engine can react to the *condition* rather than to one vendor's status code. + */ + +export abstract class ProviderError extends Error { + /** The provider that raised it, when the adapter knows its own id. */ + readonly providerId?: string + + constructor(message: string, options?: { providerId?: string; cause?: unknown }) { + super(message, options?.cause !== undefined ? { cause: options.cause } : undefined) + this.name = new.target.name + this.providerId = options?.providerId + } +} + +/** + * Credentials are gone or no longer accepted. Drives the `reconnect_required` + * account status — the user has to re-authorize, retrying will not help. + */ +export class ProviderAuthError extends ProviderError {} + +/** + * The incremental cursor the provider gave us is no longer valid (Google 410, + * CalDAV `valid-sync-token`, Graph `resyncRequired`). The engine clears + * `sync_cursor` and re-runs the source from scratch. + */ +export class ProviderGoneError extends ProviderError {} + +/** The remote copy moved under us — HTTP 412 / ETag mismatch. */ +export class ProviderConflictError extends ProviderError {} + +/** Throttled. `retryAfterMs` is the provider's own hint when it gave one. */ +export class ProviderRateLimitError extends ProviderError { + readonly retryAfterMs: number | null + + constructor( + message: string, + options?: { retryAfterMs?: number | null; providerId?: string; cause?: unknown } + ) { + super(message, options) + this.retryAfterMs = options?.retryAfterMs ?? null + } +} + +/** Network blip, 5xx, timeout — worth retrying on the next pass, nothing to report. */ +export class ProviderTransientError extends ProviderError {} diff --git a/apps/desktop/src/main/calendar/google/account-routing.test.ts b/apps/desktop/src/main/calendar/providers/google/account-routing.test.ts similarity index 100% rename from apps/desktop/src/main/calendar/google/account-routing.test.ts rename to apps/desktop/src/main/calendar/providers/google/account-routing.test.ts diff --git a/apps/desktop/src/main/calendar/google/account-routing.ts b/apps/desktop/src/main/calendar/providers/google/account-routing.ts similarity index 94% rename from apps/desktop/src/main/calendar/google/account-routing.ts rename to apps/desktop/src/main/calendar/providers/google/account-routing.ts index 3d58ccc82..4587553a3 100644 --- a/apps/desktop/src/main/calendar/google/account-routing.ts +++ b/apps/desktop/src/main/calendar/providers/google/account-routing.ts @@ -2,8 +2,8 @@ import { and, eq, isNull } from 'drizzle-orm' import { calendarEvents } from '@memry/db-schema/schema/calendar-events' import { calendarSources } from '@memry/db-schema/schema/calendar-sources' import type { calendarBindings } from '@memry/db-schema/schema/calendar-bindings' -import type { DataDb } from '../../database/types' -import type { CalendarSyncTarget } from '../types' +import type { DataDb } from '../../../database/types' +import type { CalendarSyncTarget } from '../../types' import { resolveDefaultGoogleAccountId } from './oauth' function findAccountIdForCalendarRemoteId(db: DataDb, remoteCalendarId: string): string | null { diff --git a/apps/desktop/src/main/calendar/google/calendar-google-settings.ts b/apps/desktop/src/main/calendar/providers/google/calendar-google-settings.ts similarity index 88% rename from apps/desktop/src/main/calendar/google/calendar-google-settings.ts rename to apps/desktop/src/main/calendar/providers/google/calendar-google-settings.ts index b71477eb1..e4f249301 100644 --- a/apps/desktop/src/main/calendar/google/calendar-google-settings.ts +++ b/apps/desktop/src/main/calendar/providers/google/calendar-google-settings.ts @@ -2,8 +2,8 @@ import { CALENDAR_GOOGLE_SETTINGS_DEFAULTS, type CalendarGoogleSettings } from '@memry/contracts/settings-schemas' -import { getSetting, setSetting } from '../../settings/settings-store' -import type { DataDb } from '../../database' +import { getSetting, setSetting } from '../../../settings/settings-store' +import type { DataDb } from '../../../database' const CALENDAR_GOOGLE_SETTINGS_KEY = 'calendar.google' diff --git a/apps/desktop/src/main/calendar/google/calendar-source-discovery.test.ts b/apps/desktop/src/main/calendar/providers/google/calendar-source-discovery.test.ts similarity index 97% rename from apps/desktop/src/main/calendar/google/calendar-source-discovery.test.ts rename to apps/desktop/src/main/calendar/providers/google/calendar-source-discovery.test.ts index fdd2f84fc..a9edce4e5 100644 --- a/apps/desktop/src/main/calendar/google/calendar-source-discovery.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/calendar-source-discovery.test.ts @@ -15,12 +15,12 @@ vi.mock('./oauth', () => ({ resolveDefaultGoogleAccountId: vi.fn(() => 'work@example.com') })) -vi.mock('../../sync/auth-state', () => ({ +vi.mock('../../../sync/auth-state', () => ({ isMemryUserSignedIn: vi.fn(async () => true) })) import { discoverGoogleCalendarSources } from './sync-service' -import { upsertCalendarSource } from '../repositories/calendar-sources-repository' +import { upsertCalendarSource } from '../../repositories/calendar-sources-repository' const REMOTE_CALENDARS = [ { diff --git a/apps/desktop/src/main/calendar/google/client.test.ts b/apps/desktop/src/main/calendar/providers/google/client.test.ts similarity index 99% rename from apps/desktop/src/main/calendar/google/client.test.ts rename to apps/desktop/src/main/calendar/providers/google/client.test.ts index 4e9657387..e8feac32b 100644 --- a/apps/desktop/src/main/calendar/google/client.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/client.test.ts @@ -24,7 +24,7 @@ const { loggerMock } = vi.hoisted(() => ({ } })) -vi.mock('../../lib/logger', () => ({ +vi.mock('../../../lib/logger', () => ({ createLogger: () => loggerMock })) @@ -33,7 +33,7 @@ vi.mock('../../lib/logger', () => ({ // API/token failure below throws 'main-process i18n not initialized' instead of // the mapped message — and throwCalendarApiFailure never attaches // error.status / error.apiStatus. -vi.mock('../../lib/main-i18n', () => ({ +vi.mock('../../../lib/main-i18n', () => ({ getMainI18n: () => ({ t: (key: string) => key, getFixedT: () => (key: string) => key diff --git a/apps/desktop/src/main/calendar/google/client.ts b/apps/desktop/src/main/calendar/providers/google/client.ts similarity index 97% rename from apps/desktop/src/main/calendar/google/client.ts rename to apps/desktop/src/main/calendar/providers/google/client.ts index aa68e6134..88e8aa9b3 100644 --- a/apps/desktop/src/main/calendar/google/client.ts +++ b/apps/desktop/src/main/calendar/providers/google/client.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { createLogger } from '../../lib/logger' +import { createLogger } from '../../../lib/logger' import { clearGoogleCalendarTokens, getGoogleCalendarTokens, @@ -11,7 +11,7 @@ import type { GoogleCalendarDescriptor, GoogleCalendarRemoteEvent, GoogleCalendarUpsertEventInput -} from '../types' +} from '../../types' const log = createLogger('Calendar:GoogleClient') const GOOGLE_API_BASE = 'https://www.googleapis.com/calendar/v3' @@ -465,7 +465,7 @@ export function createGoogleCalendarClient( if (!accountId || !accountId.trim()) { throw new Error('createGoogleCalendarClient requires a non-empty accountId') } - return { + const client: Omit = { async listCalendars(): Promise { const response = await withAuthorizedResponse(accountId, { path: '/users/me/calendarList' @@ -659,4 +659,14 @@ export function createGoogleCalendarClient( } } } + + // `watch`/`unwatch` are the neutral adapter names; `watchCalendar`/ + // `stopChannel` are the Google-named pair the push-channel manager and the + // sync-server relay still speak. Same implementation, two entry points, + // until the relay is generalized (#1404). + return { + ...client, + watch: client.watchCalendar, + unwatch: client.stopChannel + } } diff --git a/apps/desktop/src/main/calendar/google/google-channel-manager.test.ts b/apps/desktop/src/main/calendar/providers/google/google-channel-manager.test.ts similarity index 99% rename from apps/desktop/src/main/calendar/google/google-channel-manager.test.ts rename to apps/desktop/src/main/calendar/providers/google/google-channel-manager.test.ts index 1b0a6ee48..a83fa10d3 100644 --- a/apps/desktop/src/main/calendar/google/google-channel-manager.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/google-channel-manager.test.ts @@ -10,7 +10,7 @@ const { loggerMock } = vi.hoisted(() => ({ } })) -vi.mock('../../lib/logger', () => ({ +vi.mock('../../../lib/logger', () => ({ createLogger: () => loggerMock })) diff --git a/apps/desktop/src/main/calendar/google/google-channel-manager.ts b/apps/desktop/src/main/calendar/providers/google/google-channel-manager.ts similarity index 97% rename from apps/desktop/src/main/calendar/google/google-channel-manager.ts rename to apps/desktop/src/main/calendar/providers/google/google-channel-manager.ts index 8c3003574..d31610161 100644 --- a/apps/desktop/src/main/calendar/google/google-channel-manager.ts +++ b/apps/desktop/src/main/calendar/providers/google/google-channel-manager.ts @@ -1,5 +1,5 @@ -import { createLogger } from '../../lib/logger' -import type { GoogleCalendarClient } from '../types' +import { createLogger } from '../../../lib/logger' +import type { GoogleCalendarClient } from '../../types' const log = createLogger('Calendar:GoogleChannelManager') diff --git a/apps/desktop/src/main/calendar/google/google-sync-runner.test.ts b/apps/desktop/src/main/calendar/providers/google/google-sync-runner.test.ts similarity index 98% rename from apps/desktop/src/main/calendar/google/google-sync-runner.test.ts rename to apps/desktop/src/main/calendar/providers/google/google-sync-runner.test.ts index 3579e6d89..0f65f8312 100644 --- a/apps/desktop/src/main/calendar/google/google-sync-runner.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/google-sync-runner.test.ts @@ -33,11 +33,11 @@ vi.mock('./oauth', () => ({ resolveDefaultGoogleAccountId: vi.fn(() => null) })) -vi.mock('../../sync/auth-state', () => ({ +vi.mock('../../../sync/auth-state', () => ({ isMemryUserSignedIn: signedInMock })) -vi.mock('../../database', () => ({ +vi.mock('../../../database', () => ({ requireDatabase: vi.fn(() => mockDbHolder.db), getDatabase: vi.fn(() => mockDbHolder.db), isDatabaseInitialized: vi.fn(() => true) diff --git a/apps/desktop/src/main/calendar/google/google-sync-runner.ts b/apps/desktop/src/main/calendar/providers/google/google-sync-runner.ts similarity index 93% rename from apps/desktop/src/main/calendar/google/google-sync-runner.ts rename to apps/desktop/src/main/calendar/providers/google/google-sync-runner.ts index 5b9219d85..3d8f1ef59 100644 --- a/apps/desktop/src/main/calendar/google/google-sync-runner.ts +++ b/apps/desktop/src/main/calendar/providers/google/google-sync-runner.ts @@ -1,13 +1,13 @@ import { powerMonitor } from 'electron' import { toSafeToken } from '@memry/contracts/telemetry-api' -import { createLogger } from '../../lib/logger' -import { requireDatabase, isDatabaseInitialized } from '../../database' -import { trackMainError } from '../../telemetry/diagnostics' -import { shouldEmitThrottled } from '../../telemetry/throttle' -import { trackMainEvent } from '../../telemetry/track' -import { isMemryUserSignedIn } from '../../sync/auth-state' +import { createLogger } from '../../../lib/logger' +import { requireDatabase, isDatabaseInitialized } from '../../../database' +import { trackMainError } from '../../../telemetry/diagnostics' +import { shouldEmitThrottled } from '../../../telemetry/throttle' +import { trackMainEvent } from '../../../telemetry/track' +import { isMemryUserSignedIn } from '../../../sync/auth-state' import { hasGoogleCalendarConnection } from './oauth' -import { listCalendarSources } from '../repositories/calendar-sources-repository' +import { listCalendarSources } from '../../repositories/calendar-sources-repository' import { getGooglePushRuntime, getOrInitGooglePushRuntime } from './push-runtime' import { syncGoogleCalendarNow } from './sync-service' diff --git a/apps/desktop/src/main/calendar/google/keychain.test.ts b/apps/desktop/src/main/calendar/providers/google/keychain.test.ts similarity index 100% rename from apps/desktop/src/main/calendar/google/keychain.test.ts rename to apps/desktop/src/main/calendar/providers/google/keychain.test.ts diff --git a/apps/desktop/src/main/calendar/google/keychain.ts b/apps/desktop/src/main/calendar/providers/google/keychain.ts similarity index 97% rename from apps/desktop/src/main/calendar/google/keychain.ts rename to apps/desktop/src/main/calendar/providers/google/keychain.ts index 9b3f046be..507fc1943 100644 --- a/apps/desktop/src/main/calendar/google/keychain.ts +++ b/apps/desktop/src/main/calendar/providers/google/keychain.ts @@ -1,4 +1,4 @@ -import { deleteSecret, getSecret, setSecret } from '../../secrets/secret-storage' +import { deleteSecret, getSecret, setSecret } from '../../../secrets/secret-storage' const SERVICE = 'com.memry.calendar.google' diff --git a/apps/desktop/src/main/calendar/google/local-sync-effects.ts b/apps/desktop/src/main/calendar/providers/google/local-sync-effects.ts similarity index 77% rename from apps/desktop/src/main/calendar/google/local-sync-effects.ts rename to apps/desktop/src/main/calendar/providers/google/local-sync-effects.ts index 55efa3884..13389f9f2 100644 --- a/apps/desktop/src/main/calendar/google/local-sync-effects.ts +++ b/apps/desktop/src/main/calendar/providers/google/local-sync-effects.ts @@ -1,9 +1,9 @@ import { toErrorCode } from '@memry/contracts/telemetry-api' -import { requireDatabase } from '../../database' -import { createLogger } from '../../lib/logger' -import { trackMainError } from '../../telemetry/diagnostics' -import { shouldEmitThrottled } from '../../telemetry/throttle' -import type { CalendarSyncTarget } from '../types' +import { requireDatabase } from '../../../database' +import { createLogger } from '../../../lib/logger' +import { trackMainError } from '../../../telemetry/diagnostics' +import { shouldEmitThrottled } from '../../../telemetry/throttle' +import type { CalendarSyncTarget } from '../../types' import { syncLocalSourceToGoogleCalendar } from './sync-service' const log = createLogger('Calendar:GoogleLocalEffects') diff --git a/apps/desktop/src/main/calendar/google/mappers.test.ts b/apps/desktop/src/main/calendar/providers/google/mappers.test.ts similarity index 99% rename from apps/desktop/src/main/calendar/google/mappers.test.ts rename to apps/desktop/src/main/calendar/providers/google/mappers.test.ts index f2509942a..c3d18df07 100644 --- a/apps/desktop/src/main/calendar/google/mappers.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/mappers.test.ts @@ -4,7 +4,7 @@ import { mapGoogleEventToCalendarEventChanges, mapGoogleEventToExternalEventRecord } from './mappers' -import type { GoogleCalendarRemoteEvent } from '../types' +import type { GoogleCalendarRemoteEvent } from '../../types' import type { CalendarEvent } from '@memry/db-schema/schema/calendar-events' const LOCAL_EVENT_BASE: CalendarEvent = { diff --git a/apps/desktop/src/main/calendar/google/mappers.ts b/apps/desktop/src/main/calendar/providers/google/mappers.ts similarity index 99% rename from apps/desktop/src/main/calendar/google/mappers.ts rename to apps/desktop/src/main/calendar/providers/google/mappers.ts index c2f162372..4a9b4ed1e 100644 --- a/apps/desktop/src/main/calendar/google/mappers.ts +++ b/apps/desktop/src/main/calendar/providers/google/mappers.ts @@ -13,7 +13,7 @@ import type { CalendarSyncSourceType, GoogleCalendarRemoteEvent, GoogleCalendarUpsertEventInput -} from '../types' +} from '../../types' export interface CalendarEventChanges { title: string diff --git a/apps/desktop/src/main/calendar/google/oauth-errors.test.ts b/apps/desktop/src/main/calendar/providers/google/oauth-errors.test.ts similarity index 99% rename from apps/desktop/src/main/calendar/google/oauth-errors.test.ts rename to apps/desktop/src/main/calendar/providers/google/oauth-errors.test.ts index a1fe8b193..7dcde79fd 100644 --- a/apps/desktop/src/main/calendar/google/oauth-errors.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/oauth-errors.test.ts @@ -7,7 +7,7 @@ const { getFixedTSpy } = vi.hoisted(() => ({ getFixedTSpy: vi.fn((_lng: unknown, _ns: unknown) => (key: string) => key) })) -vi.mock('../../lib/main-i18n', () => ({ +vi.mock('../../../lib/main-i18n', () => ({ getMainI18n: () => ({ t: (key: string) => key, getFixedT: getFixedTSpy diff --git a/apps/desktop/src/main/calendar/google/oauth-errors.ts b/apps/desktop/src/main/calendar/providers/google/oauth-errors.ts similarity index 96% rename from apps/desktop/src/main/calendar/google/oauth-errors.ts rename to apps/desktop/src/main/calendar/providers/google/oauth-errors.ts index 06f92d536..cbc3e8eeb 100644 --- a/apps/desktop/src/main/calendar/google/oauth-errors.ts +++ b/apps/desktop/src/main/calendar/providers/google/oauth-errors.ts @@ -1,4 +1,4 @@ -import { getMainI18n } from '../../lib/main-i18n' +import { getMainI18n } from '../../../lib/main-i18n' export function userMessageForTokenEndpointError(args: { status: number diff --git a/apps/desktop/src/main/calendar/google/oauth-poisoned-secret.test.ts b/apps/desktop/src/main/calendar/providers/google/oauth-poisoned-secret.test.ts similarity index 97% rename from apps/desktop/src/main/calendar/google/oauth-poisoned-secret.test.ts rename to apps/desktop/src/main/calendar/providers/google/oauth-poisoned-secret.test.ts index 7f201c0cc..8169fda98 100644 --- a/apps/desktop/src/main/calendar/google/oauth-poisoned-secret.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/oauth-poisoned-secret.test.ts @@ -64,15 +64,15 @@ const { loggerMock } = vi.hoisted(() => ({ loggerMock: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } })) -vi.mock('../../lib/logger', () => ({ +vi.mock('../../../lib/logger', () => ({ createLogger: () => loggerMock })) -vi.mock('../repositories/calendar-sources-repository', () => ({ +vi.mock('../../repositories/calendar-sources-repository', () => ({ listCalendarSources: vi.fn(() => []) })) -vi.mock('../../lib/main-i18n', () => ({ +vi.mock('../../../lib/main-i18n', () => ({ getMainI18n: () => ({ t: (key: string) => key, getFixedT: () => (key: string) => key @@ -85,7 +85,7 @@ import { hasGoogleCalendarLocalAuth } from './oauth' import { getGoogleCalendarTokens, hasGoogleCalendarTokens } from './keychain' -import { SECRET_STORE_FILENAME, resetSecretStorageForTests } from '../../secrets/secret-storage' +import { SECRET_STORE_FILENAME, resetSecretStorageForTests } from '../../../secrets/secret-storage' const SERVICE = 'com.memry.calendar.google' const ACCOUNT_ID = 'user@example.com' diff --git a/apps/desktop/src/main/calendar/google/oauth.test.ts b/apps/desktop/src/main/calendar/providers/google/oauth.test.ts similarity index 99% rename from apps/desktop/src/main/calendar/google/oauth.test.ts rename to apps/desktop/src/main/calendar/providers/google/oauth.test.ts index e24653cc3..36b3a0b46 100644 --- a/apps/desktop/src/main/calendar/google/oauth.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/oauth.test.ts @@ -27,7 +27,7 @@ const { loggerMock } = vi.hoisted(() => ({ } })) -vi.mock('../../lib/logger', () => ({ +vi.mock('../../../lib/logger', () => ({ createLogger: () => loggerMock })) @@ -35,14 +35,14 @@ const { mockListCalendarSources } = vi.hoisted(() => ({ mockListCalendarSources: vi.fn() })) -vi.mock('../repositories/calendar-sources-repository', () => ({ +vi.mock('../../repositories/calendar-sources-repository', () => ({ listCalendarSources: (...args: unknown[]) => mockListCalendarSources(...args) })) // oauth-errors resolves its copy through the main-process i18n singleton, which // only exists after setMainI18n() during app boot. Echo the key back so the // assertions below pin the chosen message, not one locale's wording. -vi.mock('../../lib/main-i18n', () => ({ +vi.mock('../../../lib/main-i18n', () => ({ getMainI18n: () => ({ t: (key: string) => key, getFixedT: () => (key: string) => key @@ -61,7 +61,7 @@ import { resetGoogleCalendarOAuthState, resolveDefaultGoogleAccountId } from './oauth' -import { isExpectedConditionError } from '../../telemetry/expected-conditions' +import { isExpectedConditionError } from '../../../telemetry/expected-conditions' import { LEGACY_DEFAULT_ACCOUNT_ID, clearGoogleCalendarTokens, diff --git a/apps/desktop/src/main/calendar/google/oauth.ts b/apps/desktop/src/main/calendar/providers/google/oauth.ts similarity index 98% rename from apps/desktop/src/main/calendar/google/oauth.ts rename to apps/desktop/src/main/calendar/providers/google/oauth.ts index ee2e81eb7..4f68a6c18 100644 --- a/apps/desktop/src/main/calendar/google/oauth.ts +++ b/apps/desktop/src/main/calendar/providers/google/oauth.ts @@ -2,10 +2,10 @@ import http from 'node:http' import { createHash, randomBytes } from 'node:crypto' import { shell } from 'electron' import { z } from 'zod' -import { createLogger } from '../../lib/logger' -import { markExpectedCondition } from '../../telemetry/expected-conditions' -import type { DataDb } from '../../database/types' -import { listCalendarSources } from '../repositories/calendar-sources-repository' +import { createLogger } from '../../../lib/logger' +import { markExpectedCondition } from '../../../telemetry/expected-conditions' +import type { DataDb } from '../../../database/types' +import { listCalendarSources } from '../../repositories/calendar-sources-repository' import { clearGoogleCalendarTokens, getGoogleCalendarTokens, diff --git a/apps/desktop/src/main/calendar/google/onboarding.test.ts b/apps/desktop/src/main/calendar/providers/google/onboarding.test.ts similarity index 98% rename from apps/desktop/src/main/calendar/google/onboarding.test.ts rename to apps/desktop/src/main/calendar/providers/google/onboarding.test.ts index 630dd39a7..7ad55fd24 100644 --- a/apps/desktop/src/main/calendar/google/onboarding.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/onboarding.test.ts @@ -2,8 +2,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createTestDataDb, type TestDatabaseResult, type TestDb } from '@tests/utils/test-db' import { settings } from '@memry/db-schema/schema/settings' import { listGoogleCalendars, setDefaultGoogleCalendar } from './onboarding' -import type { GoogleCalendarDescriptor, GoogleCalendarClient } from '../types' -import type { DataDb } from '../../database' +import type { GoogleCalendarDescriptor, GoogleCalendarClient } from '../../types' +import type { DataDb } from '../../../database' function makeClient( calendars: GoogleCalendarDescriptor[] diff --git a/apps/desktop/src/main/calendar/google/onboarding.ts b/apps/desktop/src/main/calendar/providers/google/onboarding.ts similarity index 93% rename from apps/desktop/src/main/calendar/google/onboarding.ts rename to apps/desktop/src/main/calendar/providers/google/onboarding.ts index b04bf3409..93bc9972d 100644 --- a/apps/desktop/src/main/calendar/google/onboarding.ts +++ b/apps/desktop/src/main/calendar/providers/google/onboarding.ts @@ -8,10 +8,10 @@ import { CALENDAR_GOOGLE_SETTINGS_DEFAULTS, type CalendarGoogleSettings } from '@memry/contracts/settings-schemas' -import { setSetting } from '../../settings/settings-store' -import type { DataDb } from '../../database' +import { setSetting } from '../../../settings/settings-store' +import type { DataDb } from '../../../database' import { readCalendarGoogleSettings } from './calendar-google-settings' -import type { GoogleCalendarClient, GoogleCalendarDescriptor } from '../types' +import type { GoogleCalendarClient, GoogleCalendarDescriptor } from '../../types' const CALENDAR_GOOGLE_SETTINGS_KEY = 'calendar.google' diff --git a/apps/desktop/src/main/calendar/google/provider-auth-transfer.test.ts b/apps/desktop/src/main/calendar/providers/google/provider-auth-transfer.test.ts similarity index 100% rename from apps/desktop/src/main/calendar/google/provider-auth-transfer.test.ts rename to apps/desktop/src/main/calendar/providers/google/provider-auth-transfer.test.ts diff --git a/apps/desktop/src/main/calendar/google/provider-auth-transfer.ts b/apps/desktop/src/main/calendar/providers/google/provider-auth-transfer.ts similarity index 98% rename from apps/desktop/src/main/calendar/google/provider-auth-transfer.ts rename to apps/desktop/src/main/calendar/providers/google/provider-auth-transfer.ts index e9f597026..2350dbf74 100644 --- a/apps/desktop/src/main/calendar/google/provider-auth-transfer.ts +++ b/apps/desktop/src/main/calendar/providers/google/provider-auth-transfer.ts @@ -1,8 +1,8 @@ import sodium from 'libsodium-wrappers-sumo' import { z } from 'zod' -import type { DataDb } from '../../database/types' -import { computeProviderAuthConfirm, constantTimeEqual, decrypt, encrypt } from '../../crypto' +import type { DataDb } from '../../../database/types' +import { computeProviderAuthConfirm, constantTimeEqual, decrypt, encrypt } from '../../../crypto' import { getGoogleCalendarTokens, storeGoogleCalendarRefreshToken } from './keychain' import { listGoogleAccountIds } from './oauth' diff --git a/apps/desktop/src/main/calendar/google/push-conflict-retry.ts b/apps/desktop/src/main/calendar/providers/google/push-conflict-retry.ts similarity index 92% rename from apps/desktop/src/main/calendar/google/push-conflict-retry.ts rename to apps/desktop/src/main/calendar/providers/google/push-conflict-retry.ts index 8c23beedd..91a9f2624 100644 --- a/apps/desktop/src/main/calendar/google/push-conflict-retry.ts +++ b/apps/desktop/src/main/calendar/providers/google/push-conflict-retry.ts @@ -5,12 +5,15 @@ import { inboxItems } from '@memry/db-schema/schema/inbox' import { reminders } from '@memry/db-schema/schema/reminders' import { tasks } from '@memry/db-schema/schema/tasks' import type { FieldClocks } from '@memry/contracts/sync-api' -import { createLogger } from '../../lib/logger' -import type { DataDb } from '../../database/types' -import { enqueueLocalSyncUpdate } from '../../sync/local-mutations' -import { initAllFieldClocks } from '../../sync/field-merge' -import { CALENDAR_EVENT_SYNCABLE_FIELDS, mergeCalendarEventFields } from '../field-merge-calendar' -import { emitCalendarChanged } from '../change-events' +import { createLogger } from '../../../lib/logger' +import type { DataDb } from '../../../database/types' +import { enqueueLocalSyncUpdate } from '../../../sync/local-mutations' +import { initAllFieldClocks } from '../../../sync/field-merge' +import { + CALENDAR_EVENT_SYNCABLE_FIELDS, + mergeCalendarEventFields +} from '../../field-merge-calendar' +import { emitCalendarChanged } from '../../change-events' import { mapCalendarEventToGoogleInput, mapGoogleEventToCalendarEventChanges, @@ -23,7 +26,7 @@ import type { GoogleCalendarClient, GoogleCalendarRemoteEvent, GoogleCalendarUpsertEventInput -} from '../types' +} from '../../types' const log = createLogger('Calendar:GooglePushConflict') export const MAX_PUSH_CONFLICT_RETRIES = 3 diff --git a/apps/desktop/src/main/calendar/google/push-runtime.test.ts b/apps/desktop/src/main/calendar/providers/google/push-runtime.test.ts similarity index 98% rename from apps/desktop/src/main/calendar/google/push-runtime.test.ts rename to apps/desktop/src/main/calendar/providers/google/push-runtime.test.ts index 57c550478..964354cc5 100644 --- a/apps/desktop/src/main/calendar/google/push-runtime.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/push-runtime.test.ts @@ -20,17 +20,17 @@ const { loggerMock, runtimeMocks } = vi.hoisted(() => ({ } })) -vi.mock('../../lib/logger', () => ({ +vi.mock('../../../lib/logger', () => ({ createLogger: () => loggerMock })) -vi.mock('../../sync/http-client', () => ({ +vi.mock('../../../sync/http-client', () => ({ deleteFromServer: runtimeMocks.deleteFromServer, patchToServer: runtimeMocks.patchToServer, postToServer: runtimeMocks.postToServer })) -vi.mock('../../sync/token-manager', () => ({ +vi.mock('../../../sync/token-manager', () => ({ getValidAccessToken: runtimeMocks.getValidAccessToken })) @@ -42,7 +42,7 @@ vi.mock('./oauth', () => ({ resolveDefaultGoogleAccountId: runtimeMocks.resolveDefaultGoogleAccountId })) -vi.mock('../../database', () => ({ +vi.mock('../../../database', () => ({ requireDatabase: runtimeMocks.requireDatabase })) @@ -50,7 +50,7 @@ vi.mock('./google-channel-manager', () => ({ createGoogleChannelManager: runtimeMocks.createGoogleChannelManager })) -vi.mock('../repositories/calendar-sources-repository', () => ({ +vi.mock('../../repositories/calendar-sources-repository', () => ({ getCalendarSourceById: runtimeMocks.getCalendarSourceById })) diff --git a/apps/desktop/src/main/calendar/google/push-runtime.ts b/apps/desktop/src/main/calendar/providers/google/push-runtime.ts similarity index 94% rename from apps/desktop/src/main/calendar/google/push-runtime.ts rename to apps/desktop/src/main/calendar/providers/google/push-runtime.ts index 9d5b0c55b..a9989cfc5 100644 --- a/apps/desktop/src/main/calendar/google/push-runtime.ts +++ b/apps/desktop/src/main/calendar/providers/google/push-runtime.ts @@ -1,12 +1,12 @@ import { randomBytes, createHmac, randomUUID } from 'node:crypto' -import { createLogger } from '../../lib/logger' -import { deleteFromServer, patchToServer, postToServer } from '../../sync/http-client' -import { getValidAccessToken } from '../../sync/token-manager' +import { createLogger } from '../../../lib/logger' +import { deleteFromServer, patchToServer, postToServer } from '../../../sync/http-client' +import { getValidAccessToken } from '../../../sync/token-manager' import { createGoogleCalendarClient } from './client' import { resolveDefaultGoogleAccountId } from './oauth' -import { requireDatabase } from '../../database' +import { requireDatabase } from '../../../database' import { createGoogleChannelManager, type GoogleChannelManager } from './google-channel-manager' -import { getCalendarSourceById } from '../repositories/calendar-sources-repository' +import { getCalendarSourceById } from '../../repositories/calendar-sources-repository' const log = createLogger('Calendar:GooglePushRuntime') diff --git a/apps/desktop/src/main/calendar/google/sync-service-cadence.test.ts b/apps/desktop/src/main/calendar/providers/google/sync-service-cadence.test.ts similarity index 98% rename from apps/desktop/src/main/calendar/google/sync-service-cadence.test.ts rename to apps/desktop/src/main/calendar/providers/google/sync-service-cadence.test.ts index 8c00684b2..da3ff915f 100644 --- a/apps/desktop/src/main/calendar/google/sync-service-cadence.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/sync-service-cadence.test.ts @@ -20,11 +20,11 @@ vi.mock('./oauth', () => ({ resolveDefaultGoogleAccountId: vi.fn(() => null) })) -vi.mock('../../sync/auth-state', () => ({ +vi.mock('../../../sync/auth-state', () => ({ isMemryUserSignedIn: vi.fn(async () => true) })) -vi.mock('../../database', () => ({ +vi.mock('../../../database', () => ({ requireDatabase: vi.fn(() => mockDbHolder.db), getDatabase: vi.fn(() => mockDbHolder.db), isDatabaseInitialized: vi.fn(() => true) diff --git a/apps/desktop/src/main/calendar/google/sync-service-trigger.test.ts b/apps/desktop/src/main/calendar/providers/google/sync-service-trigger.test.ts similarity index 98% rename from apps/desktop/src/main/calendar/google/sync-service-trigger.test.ts rename to apps/desktop/src/main/calendar/providers/google/sync-service-trigger.test.ts index 31aecec4d..b647e8775 100644 --- a/apps/desktop/src/main/calendar/google/sync-service-trigger.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/sync-service-trigger.test.ts @@ -34,13 +34,13 @@ vi.mock('./oauth', () => ({ resolveDefaultGoogleAccountId: vi.fn(() => null) })) -vi.mock('../../sync/auth-state', () => ({ +vi.mock('../../../sync/auth-state', () => ({ isMemryUserSignedIn: vi.fn(async () => true) })) const isDatabaseInitializedMock = vi.fn(() => true) -vi.mock('../../database', () => ({ +vi.mock('../../../database', () => ({ requireDatabase: vi.fn(() => mockDbHolder.db), getDatabase: vi.fn(() => mockDbHolder.db), isDatabaseInitialized: () => isDatabaseInitializedMock() diff --git a/apps/desktop/src/main/calendar/google/sync-service.test.ts b/apps/desktop/src/main/calendar/providers/google/sync-service.test.ts similarity index 99% rename from apps/desktop/src/main/calendar/google/sync-service.test.ts rename to apps/desktop/src/main/calendar/providers/google/sync-service.test.ts index 326aae7a6..337b21d6c 100644 --- a/apps/desktop/src/main/calendar/google/sync-service.test.ts +++ b/apps/desktop/src/main/calendar/providers/google/sync-service.test.ts @@ -44,7 +44,7 @@ vi.mock('./oauth', () => ({ listGoogleAccountIds: vi.fn(() => ['test-account@example.com']) })) -vi.mock('../../sync/auth-state', () => ({ +vi.mock('../../../sync/auth-state', () => ({ isMemryUserSignedIn: vi.fn(async () => true) })) @@ -55,7 +55,7 @@ import { } from './oauth' import { resolveDefaultGoogleAccountId } from './oauth' import * as googleClientModule from './client' -import { isMemryUserSignedIn } from '../../sync/auth-state' +import { isMemryUserSignedIn } from '../../../sync/auth-state' import { applyGoogleCalendarDelete, applyGoogleCalendarWriteback, @@ -65,8 +65,8 @@ import { syncGoogleCalendarNow, syncGoogleCalendarSource } from './sync-service' -import { SyncQueueManager } from '../../sync/queue' -import { initReminderSyncService, resetReminderSyncService } from '../../sync/reminder-sync' +import { SyncQueueManager } from '../../../sync/queue' +import { initReminderSyncService, resetReminderSyncService } from '../../../sync/reminder-sync' describe('google calendar sync service', () => { let dbResult: TestDatabaseResult diff --git a/apps/desktop/src/main/calendar/google/sync-service.ts b/apps/desktop/src/main/calendar/providers/google/sync-service.ts similarity index 97% rename from apps/desktop/src/main/calendar/google/sync-service.ts rename to apps/desktop/src/main/calendar/providers/google/sync-service.ts index 0489cd048..81ca21744 100644 --- a/apps/desktop/src/main/calendar/google/sync-service.ts +++ b/apps/desktop/src/main/calendar/providers/google/sync-service.ts @@ -5,16 +5,16 @@ import { calendarSources } from '@memry/db-schema/schema/calendar-sources' import { inboxItems } from '@memry/db-schema/schema/inbox' import { reminders } from '@memry/db-schema/schema/reminders' import { tasks } from '@memry/db-schema/schema/tasks' -import { createLogger } from '../../lib/logger' -import { requireDatabase, type DataDb } from '../../database' +import { createLogger } from '../../../lib/logger' +import { requireDatabase, type DataDb } from '../../../database' import { enqueueLocalSyncCreate, enqueueLocalSyncDelete, enqueueLocalSyncUpdate -} from '../../sync/local-mutations' -import { getCurrentDeviceId } from '../../sync/current-device-id' -import { recordExternalTaskUpdate } from '../../tasks/activity-log' -import { publishProjectionEvent } from '../../projections' +} from '../../../sync/local-mutations' +import { getCurrentDeviceId } from '../../../sync/current-device-id' +import { recordExternalTaskUpdate } from '../../../tasks/activity-log' +import { publishProjectionEvent } from '../../../projections' import { hasGoogleCalendarConnection, listGoogleAccountIds, @@ -22,10 +22,10 @@ import { } from './oauth' import { resolveTargetGoogleAccountId } from './account-routing' import { loadSourceAsGoogleEvent, pushEventWithConflictRetry } from './push-conflict-retry' -import { isMemryUserSignedIn } from '../../sync/auth-state' -import { increment } from '../../sync/vector-clock' +import { isMemryUserSignedIn } from '../../../sync/auth-state' +import { increment } from '../../../sync/vector-clock' import { createGoogleCalendarClient } from './client' -import { CALENDAR_EVENT_SYNCABLE_FIELDS } from '../field-merge-calendar' +import { CALENDAR_EVENT_SYNCABLE_FIELDS } from '../../field-merge-calendar' import { mapGoogleEventToCalendarEventChanges, mapGoogleEventToExternalEventRecord, @@ -35,7 +35,7 @@ import { import { getCalendarExternalEventById, upsertCalendarExternalEvent -} from '../repositories/calendar-external-events-repository' +} from '../../repositories/calendar-external-events-repository' import { findCalendarBindingByRemoteEvent, getCalendarSourceById, @@ -43,10 +43,14 @@ import { listCalendarSources, upsertCalendarBinding, upsertCalendarSource -} from '../repositories/calendar-sources-repository' -import { emitCalendarChanged, emitCalendarProjectionChanged } from '../change-events' +} from '../../repositories/calendar-sources-repository' +import { emitCalendarChanged, emitCalendarProjectionChanged } from '../../change-events' import { readCalendarGoogleSettings } from './calendar-google-settings' -import type { CalendarSyncTarget, GoogleCalendarClient, GoogleCalendarRemoteEvent } from '../types' +import type { + CalendarSyncTarget, + GoogleCalendarClient, + GoogleCalendarRemoteEvent +} from '../../types' const log = createLogger('Calendar:GoogleSync') const LOCAL_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' diff --git a/apps/desktop/src/main/calendar/runtime-effects.ts b/apps/desktop/src/main/calendar/runtime-effects.ts index d25ef101b..50d1fc826 100644 --- a/apps/desktop/src/main/calendar/runtime-effects.ts +++ b/apps/desktop/src/main/calendar/runtime-effects.ts @@ -3,7 +3,7 @@ import { enqueueLocalSyncDelete, enqueueLocalSyncUpdate } from '../sync/local-mutations' -import { scheduleGoogleCalendarSourceSync } from './google/local-sync-effects' +import { scheduleGoogleCalendarSourceSync } from './providers/google/local-sync-effects' export function syncCalendarEventCreate(eventId: string): void { enqueueLocalSyncCreate('calendar_event', eventId) diff --git a/apps/desktop/src/main/calendar/types.ts b/apps/desktop/src/main/calendar/types.ts index ded768e9a..3503fe92b 100644 --- a/apps/desktop/src/main/calendar/types.ts +++ b/apps/desktop/src/main/calendar/types.ts @@ -1,9 +1,11 @@ import type { - CalendarAttendee, - CalendarConferenceData, - CalendarReminders, - CalendarVisibility -} from '@memry/db-schema/schema/calendar-events' + CalendarProviderAdapter, + RemoteCalendarDescriptor, + RemoteCalendarEvent, + UpsertRemoteEventInput, + WatchCalendarInput, + WatchCalendarResult +} from './provider/adapter' export type CalendarSyncSourceType = 'event' | 'task' | 'reminder' | 'inbox_snooze' @@ -12,80 +14,31 @@ export interface CalendarSyncTarget { sourceId: string } -export interface GoogleCalendarDescriptor { - id: string - title: string - timezone: string | null - color: string | null - isPrimary: boolean -} - -export interface GoogleCalendarRemoteEvent { - id: string - calendarId: string - title: string - description: string | null - location: string | null - startAt: string - endAt: string | null - isAllDay: boolean - timezone: string - status: 'confirmed' | 'tentative' | 'cancelled' - etag: string | null - updatedAt: string | null - attendees: CalendarAttendee[] | null - reminders: CalendarReminders | null - visibility: CalendarVisibility | null - colorId: string | null - conferenceData: CalendarConferenceData | null - recurringEventId: string | null - originalStartTime: string | null - raw: Record -} +// The remote-event shapes are provider-neutral now (`provider/adapter.ts`). +// These aliases keep the Google-era names alive for the existing Google call +// sites; new code should import the neutral names directly. +export type GoogleCalendarDescriptor = RemoteCalendarDescriptor +export type GoogleCalendarRemoteEvent = RemoteCalendarEvent +export type GoogleCalendarUpsertEventInput = UpsertRemoteEventInput -export interface GoogleCalendarUpsertEventInput { - sourceType: CalendarSyncSourceType - sourceId: string - title: string - description: string | null - location: string | null - startAt: string - endAt: string | null - isAllDay: boolean - timezone: string - recurrence: string[] | null - attendees?: CalendarAttendee[] | null - reminders?: CalendarReminders | null - visibility?: CalendarVisibility | null - colorId?: string | null - conferenceData?: CalendarConferenceData | null - recurringEventId?: string | null - originalStartTime?: string | null -} - -export interface GoogleCalendarClient { - listCalendars(): Promise - createCalendar(input: { title: string; timezone: string }): Promise - listEvents(input: { - calendarId: string - syncCursor?: string | null - timeMin?: string | null - timeMax?: string | null - }): Promise<{ events: GoogleCalendarRemoteEvent[]; nextSyncCursor: string | null }> - getEvent(input: { calendarId: string; eventId: string }): Promise +/** + * Google's adapter. Everything optional on `CalendarProviderAdapter` is + * required here — Google writes, creates calendars and pushes — plus the two + * Google-named push-channel methods the channel manager still calls. Those + * stay until the sync-server relay is generalized (#1404); `watch`/`unwatch` + * are the neutral names the engine sees. + */ +export interface GoogleCalendarClient extends CalendarProviderAdapter { + createCalendar(input: { title: string; timezone: string }): Promise upsertEvent(input: { calendarId: string eventId: string | null - event: GoogleCalendarUpsertEventInput + event: UpsertRemoteEventInput ifMatch?: string | null - }): Promise + }): Promise deleteEvent(input: { calendarId: string; eventId: string }): Promise - watchCalendar(input: { - calendarId: string - channelId: string - token: string - webhookUrl: string - ttlSeconds: number - }): Promise<{ resourceId: string; expiration: number }> + watch(input: WatchCalendarInput): Promise + unwatch(input: { channelId: string; resourceId: string }): Promise + watchCalendar(input: WatchCalendarInput): Promise stopChannel(input: { channelId: string; resourceId: string }): Promise } diff --git a/apps/desktop/src/main/import/onenote/onenote-auth.ts b/apps/desktop/src/main/import/onenote/onenote-auth.ts index 8b222cf36..24540d41b 100644 --- a/apps/desktop/src/main/import/onenote/onenote-auth.ts +++ b/apps/desktop/src/main/import/onenote/onenote-auth.ts @@ -2,7 +2,7 @@ * Microsoft account auth for the OneNote importer. * * Auth-code + PKCE flow for a public desktop client (no client secret), - * mirroring `calendar/google/oauth.ts`: an ephemeral loopback HTTP server + * mirroring `calendar/providers/google/oauth.ts`: an ephemeral loopback HTTP server * receives the redirect, the state is single-use with a 10-minute expiry, and * the consent screen opens in the system browser. Tokens live in Memry's * secret storage (Electron `safeStorage` with keytar fallback) under a diff --git a/apps/desktop/src/main/inbox/runtime-effects.ts b/apps/desktop/src/main/inbox/runtime-effects.ts index 15f0b3daa..b6764d25a 100644 --- a/apps/desktop/src/main/inbox/runtime-effects.ts +++ b/apps/desktop/src/main/inbox/runtime-effects.ts @@ -1,6 +1,6 @@ import { publishProjectionEvent } from '../projections' import { emitCalendarProjectionChanged } from '../calendar/change-events' -import { scheduleGoogleCalendarSourceSync } from '../calendar/google/local-sync-effects' +import { scheduleGoogleCalendarSourceSync } from '../calendar/providers/google/local-sync-effects' import { enqueueLocalSyncCreate, enqueueLocalSyncDelete, diff --git a/apps/desktop/src/main/inbox/sync-enqueue-guard.test.ts b/apps/desktop/src/main/inbox/sync-enqueue-guard.test.ts index cbd75ca74..61f05e080 100644 --- a/apps/desktop/src/main/inbox/sync-enqueue-guard.test.ts +++ b/apps/desktop/src/main/inbox/sync-enqueue-guard.test.ts @@ -39,7 +39,7 @@ const INBOX_DIR = path.dirname(fileURLToPath(import.meta.url)) /** * The fence is the whole main process, not `inbox/`. `readdirSync(INBOX_DIR)` * was both non-recursive and directory-scoped, so the Google Calendar - * writeback (`calendar/google/sync-service.ts`) — which clears `snoozedUntil` + * writeback (`calendar/providers/google/sync-service.ts`) — which clears `snoozedUntil` * on `inbox_items` — sat outside it entirely. Any module that can write triage * state is in scope; the `inboxItems` prefilter keeps the scan to the handful * of files that touch the table. @@ -208,7 +208,7 @@ describe('inbox triage writes enqueue a sync push', () => { 'inbox/crud.ts', 'inbox/filing.ts', 'inbox/snooze.ts', - 'calendar/google/sync-service.ts', + 'calendar/providers/google/sync-service.ts', 'sync/item-handlers/inbox-handler.ts' ]) ) diff --git a/apps/desktop/src/main/index.phase2.test.ts b/apps/desktop/src/main/index.phase2.test.ts index 34cc7c6f7..5fc1a4f3d 100644 --- a/apps/desktop/src/main/index.phase2.test.ts +++ b/apps/desktop/src/main/index.phase2.test.ts @@ -255,7 +255,7 @@ vi.mock('./telemetry/log-ship', () => ({ getLogShip: vi.fn(() => ({ dispose: vi.fn(async () => undefined) })) })) -vi.mock('./calendar/google/sync-service', () => ({ +vi.mock('./calendar/providers/google/sync-service', () => ({ startGoogleCalendarSyncRunner: startGoogleCalendarSyncRunnerMock, stopGoogleCalendarSyncRunner: stopGoogleCalendarSyncRunnerMock, triggerGoogleCalendarSyncNow: triggerGoogleCalendarSyncNowMock diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 41b1c470b..271a64dbb 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -81,7 +81,7 @@ import { startGoogleCalendarSyncRunner, stopGoogleCalendarSyncRunner, triggerGoogleCalendarSyncNow -} from './calendar/google/sync-service' +} from './calendar/providers/google/sync-service' import { log, createLogger, @@ -609,8 +609,7 @@ const DEFAULT_MAIN_WINDOW_SIZE = { width: 1550, height: 900 } as const const VAULT_PICKER_WINDOW_SIZE = { width: 760, height: 560 } as const function getInitialMainWindowSize(): - | typeof DEFAULT_MAIN_WINDOW_SIZE - | typeof VAULT_PICKER_WINDOW_SIZE { + typeof DEFAULT_MAIN_WINDOW_SIZE | typeof VAULT_PICKER_WINDOW_SIZE { if (process.env.MEMRY_FORCE_VAULT_PICKER === '1') return VAULT_PICKER_WINDOW_SIZE return getCurrentVaultPath() ? DEFAULT_MAIN_WINDOW_SIZE : VAULT_PICKER_WINDOW_SIZE } diff --git a/apps/desktop/src/main/ipc/auth-oauth-handlers.test.ts b/apps/desktop/src/main/ipc/auth-oauth-handlers.test.ts index 28cb61c13..a53dee47a 100644 --- a/apps/desktop/src/main/ipc/auth-oauth-handlers.test.ts +++ b/apps/desktop/src/main/ipc/auth-oauth-handlers.test.ts @@ -71,7 +71,7 @@ vi.mock('../sync/token-manager', () => ({ })) const mockStartGoogleRunner = vi.fn() -vi.mock('../calendar/google/sync-service', () => ({ +vi.mock('../calendar/providers/google/sync-service', () => ({ startGoogleCalendarSyncRunner: (...args: unknown[]) => mockStartGoogleRunner(...args) })) diff --git a/apps/desktop/src/main/ipc/auth-oauth-handlers.ts b/apps/desktop/src/main/ipc/auth-oauth-handlers.ts index 663068f4f..6f01a7fd7 100644 --- a/apps/desktop/src/main/ipc/auth-oauth-handlers.ts +++ b/apps/desktop/src/main/ipc/auth-oauth-handlers.ts @@ -16,7 +16,7 @@ import { store } from '../store' import { postToServer } from '../sync/http-client' import { resolveSyncServerUrl } from '../sync/sync-server-url' import { getSyncEngine, startSyncRuntime } from '../sync/runtime' -import { startGoogleCalendarSyncRunner } from '../calendar/google/sync-service' +import { startGoogleCalendarSyncRunner } from '../calendar/providers/google/sync-service' import { teardownSession } from '../sync/session-teardown' import { refreshAccessToken, storeToken } from '../sync/token-manager' import { getSetupDevicePublicKey } from '../sync/setup-token' diff --git a/apps/desktop/src/main/ipc/calendar-handlers.test.ts b/apps/desktop/src/main/ipc/calendar-handlers.test.ts index 64932b20a..e855eea6c 100644 --- a/apps/desktop/src/main/ipc/calendar-handlers.test.ts +++ b/apps/desktop/src/main/ipc/calendar-handlers.test.ts @@ -76,7 +76,7 @@ vi.mock('../sync/local-mutations', () => ({ enqueueLocalSyncDelete: vi.fn() })) -vi.mock('../calendar/google/oauth', () => ({ +vi.mock('../calendar/providers/google/oauth', () => ({ connectGoogleCalendar: (...args: unknown[]) => mockConnectGoogleCalendar(...args), disconnectGoogleCalendar: (...args: unknown[]) => mockDisconnectGoogleCalendar(...args), hasGoogleCalendarLocalAuth: (...args: unknown[]) => mockHasGoogleCalendarLocalAuth(...args), @@ -85,7 +85,7 @@ vi.mock('../calendar/google/oauth', () => ({ resolveDefaultGoogleAccountId: (...args: unknown[]) => mockResolveDefaultGoogleAccountId(...args) })) -vi.mock('../calendar/google/sync-service', () => ({ +vi.mock('../calendar/providers/google/sync-service', () => ({ discoverGoogleCalendarSources: (...args: unknown[]) => mockDiscoverGoogleCalendarSources(...args), syncGoogleCalendarNow: (...args: unknown[]) => mockSyncGoogleCalendarNow(...args), syncGoogleCalendarSource: (...args: unknown[]) => mockSyncGoogleCalendarSource(...args), @@ -95,16 +95,16 @@ vi.mock('../calendar/google/sync-service', () => ({ stopGoogleCalendarSyncRunner: (...args: unknown[]) => mockStopGoogleCalendarSyncRunner(...args) })) -vi.mock('../calendar/google/onboarding', () => ({ +vi.mock('../calendar/providers/google/onboarding', () => ({ listGoogleCalendars: (...args: unknown[]) => mockListGoogleCalendars(...args), setDefaultGoogleCalendar: (...args: unknown[]) => mockSetDefaultGoogleCalendar(...args) })) -vi.mock('../calendar/google/client', () => ({ +vi.mock('../calendar/providers/google/client', () => ({ createGoogleCalendarClient: (...args: unknown[]) => mockCreateGoogleCalendarClient(...args) })) -vi.mock('../calendar/google/push-runtime', () => ({ +vi.mock('../calendar/providers/google/push-runtime', () => ({ getGooglePushRuntime: vi.fn(() => ({ handleSelectionToggle: (...args: unknown[]) => mockPushSelectionToggle(...args) })) diff --git a/apps/desktop/src/main/ipc/calendar-handlers.ts b/apps/desktop/src/main/ipc/calendar-handlers.ts index 0738d8cb6..11a6bd24c 100644 --- a/apps/desktop/src/main/ipc/calendar-handlers.ts +++ b/apps/desktop/src/main/ipc/calendar-handlers.ts @@ -59,7 +59,7 @@ import { hasGoogleCalendarLocalAuth, listGoogleAccountIds, resolveDefaultGoogleAccountId -} from '../calendar/google/oauth' +} from '../calendar/providers/google/oauth' import { getCalendarRangeProjection } from '../calendar/projection' import { getCalendarEnabledPropertyNames } from '../calendar/calendar-property-visibility' import { getCalendarSettings } from './settings-handlers' @@ -69,10 +69,13 @@ import { stopGoogleCalendarSyncRunner, syncGoogleCalendarNow, syncGoogleCalendarSource -} from '../calendar/google/sync-service' -import { listGoogleCalendars, setDefaultGoogleCalendar } from '../calendar/google/onboarding' -import { createGoogleCalendarClient } from '../calendar/google/client' -import { getGooglePushRuntime } from '../calendar/google/push-runtime' +} from '../calendar/providers/google/sync-service' +import { + listGoogleCalendars, + setDefaultGoogleCalendar +} from '../calendar/providers/google/onboarding' +import { createGoogleCalendarClient } from '../calendar/providers/google/client' +import { getGooglePushRuntime } from '../calendar/providers/google/push-runtime' import { promoteExternalEvent, ExternalEventNotFoundError, diff --git a/apps/desktop/src/main/lib/reminders.test.ts b/apps/desktop/src/main/lib/reminders.test.ts index bb38c1737..4b0204ff4 100644 --- a/apps/desktop/src/main/lib/reminders.test.ts +++ b/apps/desktop/src/main/lib/reminders.test.ts @@ -30,7 +30,7 @@ vi.mock('../calendar/change-events', () => ({ emitCalendarProjectionChanged })) -vi.mock('../calendar/google/local-sync-effects', () => ({ +vi.mock('../calendar/providers/google/local-sync-effects', () => ({ scheduleGoogleCalendarSourceSync })) diff --git a/apps/desktop/src/main/lib/reminders.ts b/apps/desktop/src/main/lib/reminders.ts index 94af00c56..fe4cc0858 100644 --- a/apps/desktop/src/main/lib/reminders.ts +++ b/apps/desktop/src/main/lib/reminders.ts @@ -36,7 +36,7 @@ import { getMainI18n } from './main-i18n' import { publishProjectionEvent } from '../projections' import { registerMinuteTick, unregisterMinuteTick, hasMinuteTick } from './minute-tick' import { emitCalendarProjectionChanged } from '../calendar/change-events' -import { scheduleGoogleCalendarSourceSync } from '../calendar/google/local-sync-effects' +import { scheduleGoogleCalendarSourceSync } from '../calendar/providers/google/local-sync-effects' import { enqueueLocalSyncCreate, enqueueLocalSyncDelete, diff --git a/apps/desktop/src/main/secrets/secret-storage-callsites.test.ts b/apps/desktop/src/main/secrets/secret-storage-callsites.test.ts index c5baae3f2..d587b4b89 100644 --- a/apps/desktop/src/main/secrets/secret-storage-callsites.test.ts +++ b/apps/desktop/src/main/secrets/secret-storage-callsites.test.ts @@ -59,7 +59,10 @@ vi.mock('../store', () => ({ import { resetSecretStorageForTests, SECRET_STORE_FILENAME } from './secret-storage' import { confirmMasterKeyMigrated, deleteKey, retrieveKey, storeKey } from '../crypto/keychain' -import { getGoogleCalendarTokens, storeGoogleCalendarTokens } from '../calendar/google/keychain' +import { + getGoogleCalendarTokens, + storeGoogleCalendarTokens +} from '../calendar/providers/google/keychain' import { getVoiceTranscriptionOpenAIApiKey, setVoiceTranscriptionOpenAIApiKey @@ -266,7 +269,7 @@ describe('secret-storage call sites — account string preservation and migratio // Google Calendar tokens // -------------------------------------------------------------------------- - describe('calendar/google/keychain', () => { + describe('calendar/providers/google/keychain', () => { it('preserves the per-account per-kind per-device account strings', async () => { process.env.MEMRY_DEVICE = 'A' diff --git a/apps/desktop/src/main/sync/calendar-event-sync.test.ts b/apps/desktop/src/main/sync/calendar-event-sync.test.ts index c89292bde..9ca624b5c 100644 --- a/apps/desktop/src/main/sync/calendar-event-sync.test.ts +++ b/apps/desktop/src/main/sync/calendar-event-sync.test.ts @@ -295,7 +295,7 @@ describe('CalendarEventSyncService', () => { describe('#given an event pinned to a specific Google calendar', () => { // REGRESSION GUARD. `targetCalendarId` is what routes a push to the right - // Google calendar (see calendar/google/account-routing.ts and + // Google calendar (see calendar/providers/google/account-routing.ts and // sync-service.ts), and promote-external-event.ts sets it. serialize() ships // the whole row so the column does leave this device — but // CalendarEventSyncPayloadSchema used to have no `targetCalendarId` key, so diff --git a/apps/desktop/src/main/sync/current-device-id.ts b/apps/desktop/src/main/sync/current-device-id.ts index 18305d859..37e0ae279 100644 --- a/apps/desktop/src/main/sync/current-device-id.ts +++ b/apps/desktop/src/main/sync/current-device-id.ts @@ -10,7 +10,7 @@ import type { DataDb } from '../database' * * This module exists so there is exactly one copy: the same three-line query * had been pasted into `sync/runtime.ts`, `sync/offline-clock.ts` and - * `calendar/google/sync-service.ts`. It deliberately sits outside + * `calendar/providers/google/sync-service.ts`. It deliberately sits outside * `blockedFeatureSyncImports` (see scripts/check-architecture-boundaries.js) so * feature modules can import it without reaching into a sync service. */ diff --git a/apps/desktop/src/main/sync/device-registration.test.ts b/apps/desktop/src/main/sync/device-registration.test.ts index 48499a41e..cd7bb6e1e 100644 --- a/apps/desktop/src/main/sync/device-registration.test.ts +++ b/apps/desktop/src/main/sync/device-registration.test.ts @@ -114,7 +114,7 @@ vi.mock('./key-verification', () => ({ persistAccountKeyVerifier: (...args: unknown[]) => mocks.persistAccountKeyVerifier(...args) })) -vi.mock('../calendar/google/sync-service', () => ({ +vi.mock('../calendar/providers/google/sync-service', () => ({ startGoogleCalendarSyncRunner: (...args: unknown[]) => mocks.startGoogleCalendarSyncRunner(...args) })) diff --git a/apps/desktop/src/main/sync/device-registration.ts b/apps/desktop/src/main/sync/device-registration.ts index 231b5e39d..4083e5539 100644 --- a/apps/desktop/src/main/sync/device-registration.ts +++ b/apps/desktop/src/main/sync/device-registration.ts @@ -30,7 +30,7 @@ import { persistAccountKeyVerifier } from './key-verification' import { getSyncEngine, startSyncRuntime } from './runtime' -import { startGoogleCalendarSyncRunner } from '../calendar/google/sync-service' +import { startGoogleCalendarSyncRunner } from '../calendar/providers/google/sync-service' import { getOrCreateVaultUuid } from '../agent/storage/vault-id' import { ACCESS_TOKEN_EXPIRY_SECONDS, diff --git a/apps/desktop/src/main/sync/linking-service.test.ts b/apps/desktop/src/main/sync/linking-service.test.ts index 9fed902f1..52948f096 100644 --- a/apps/desktop/src/main/sync/linking-service.test.ts +++ b/apps/desktop/src/main/sync/linking-service.test.ts @@ -92,7 +92,7 @@ vi.mock('../database/client', () => ({ getDatabase: mockGetDatabase })) -vi.mock('../calendar/google/provider-auth-transfer', () => ({ +vi.mock('../calendar/providers/google/provider-auth-transfer', () => ({ collectGoogleProviderAuthTransfer: mockCollectGoogleProviderAuthTransfer, encryptGoogleProviderAuthTransfer: mockEncryptGoogleProviderAuthTransfer, decryptGoogleProviderAuthTransfer: mockDecryptGoogleProviderAuthTransfer, diff --git a/apps/desktop/src/main/sync/linking-service.ts b/apps/desktop/src/main/sync/linking-service.ts index 2dd4c95c9..a81f1da9b 100644 --- a/apps/desktop/src/main/sync/linking-service.ts +++ b/apps/desktop/src/main/sync/linking-service.ts @@ -27,13 +27,13 @@ import { secureCleanup } from '../crypto' import { getDatabase } from '../database/client' -import type { GoogleProviderAuthTransfer } from '../calendar/google/provider-auth-transfer' +import type { GoogleProviderAuthTransfer } from '../calendar/providers/google/provider-auth-transfer' import { collectGoogleProviderAuthTransfer, decryptGoogleProviderAuthTransfer, encryptGoogleProviderAuthTransfer, persistImportedGoogleProviderAuth -} from '../calendar/google/provider-auth-transfer' +} from '../calendar/providers/google/provider-auth-transfer' import { createLogger } from '../lib/logger' import { getFromServer, postToServer, RateLimitError, SyncServerError } from './http-client' diff --git a/apps/desktop/src/main/sync/runtime.test.ts b/apps/desktop/src/main/sync/runtime.test.ts index 0cee57e67..040fe3103 100644 --- a/apps/desktop/src/main/sync/runtime.test.ts +++ b/apps/desktop/src/main/sync/runtime.test.ts @@ -218,7 +218,7 @@ vi.mock('../agent/storage/vault-id', () => ({ getOrCreateVaultUuid: runtimeMocks.getOrCreateVaultUuid })) -vi.mock('../calendar/google/sync-service', () => ({ +vi.mock('../calendar/providers/google/sync-service', () => ({ syncGoogleCalendarSource: runtimeMocks.syncGoogleCalendarSource })) diff --git a/apps/desktop/src/main/sync/runtime.ts b/apps/desktop/src/main/sync/runtime.ts index 62ad19600..4e2aa4505 100644 --- a/apps/desktop/src/main/sync/runtime.ts +++ b/apps/desktop/src/main/sync/runtime.ts @@ -16,7 +16,7 @@ import { } from '../crypto' import { SyncEngine, type SyncEngineDeps } from './engine' import { resolveSyncServerUrl } from './sync-server-url' -import { syncGoogleCalendarSource } from '../calendar/google/sync-service' +import { syncGoogleCalendarSource } from '../calendar/providers/google/sync-service' import { toErrorCode } from '@memry/contracts/telemetry-api' import { trackMainEvent } from '../telemetry/track' import { SyncQueueManager } from './queue' diff --git a/apps/desktop/src/main/sync/session-teardown.test.ts b/apps/desktop/src/main/sync/session-teardown.test.ts index c366e9697..3fe79fc28 100644 --- a/apps/desktop/src/main/sync/session-teardown.test.ts +++ b/apps/desktop/src/main/sync/session-teardown.test.ts @@ -68,12 +68,12 @@ vi.mock('../store', () => ({ } })) -vi.mock('../calendar/google/oauth', () => ({ +vi.mock('../calendar/providers/google/oauth', () => ({ disconnectGoogleCalendar: (...args: unknown[]) => mocks.disconnectGoogleCalendar(...args), listGoogleAccountIds: (...args: unknown[]) => mocks.listGoogleAccountIds(...args) })) -vi.mock('../calendar/google/sync-service', () => ({ +vi.mock('../calendar/providers/google/sync-service', () => ({ stopGoogleCalendarSyncRunner: (...args: unknown[]) => mocks.stopGoogleCalendarSyncRunner(...args) })) diff --git a/apps/desktop/src/main/sync/session-teardown.ts b/apps/desktop/src/main/sync/session-teardown.ts index ae84794cd..f60703a14 100644 --- a/apps/desktop/src/main/sync/session-teardown.ts +++ b/apps/desktop/src/main/sync/session-teardown.ts @@ -14,8 +14,8 @@ import { clearInMemoryAuthState } from '../ipc/sync-core-handlers' import { getDatabase, isDatabaseInitialized } from '../database/client' import { store } from '../store' import { createLogger } from '../lib/logger' -import { disconnectGoogleCalendar, listGoogleAccountIds } from '../calendar/google/oauth' -import { stopGoogleCalendarSyncRunner } from '../calendar/google/sync-service' +import { disconnectGoogleCalendar, listGoogleAccountIds } from '../calendar/providers/google/oauth' +import { stopGoogleCalendarSyncRunner } from '../calendar/providers/google/sync-service' const log = createLogger('SessionTeardown') diff --git a/apps/desktop/src/main/tasks/runtime-effects.ts b/apps/desktop/src/main/tasks/runtime-effects.ts index c2e917989..51ed9f6e9 100644 --- a/apps/desktop/src/main/tasks/runtime-effects.ts +++ b/apps/desktop/src/main/tasks/runtime-effects.ts @@ -1,6 +1,6 @@ import { publishProjectionEvent } from '../projections' import { emitCalendarProjectionChanged } from '../calendar/change-events' -import { scheduleGoogleCalendarSourceSync } from '../calendar/google/local-sync-effects' +import { scheduleGoogleCalendarSourceSync } from '../calendar/providers/google/local-sync-effects' import { enqueueLocalSyncCreate, enqueueLocalSyncDelete, diff --git a/apps/desktop/src/main/test-hooks.test.ts b/apps/desktop/src/main/test-hooks.test.ts index 42fbf54d0..745c2c275 100644 --- a/apps/desktop/src/main/test-hooks.test.ts +++ b/apps/desktop/src/main/test-hooks.test.ts @@ -109,7 +109,7 @@ vi.mock('@memry/storage-data', () => ({ getNoteMetadataById: getNoteMetadataByIdMock })) -vi.mock('./calendar/google/keychain', () => ({ +vi.mock('./calendar/providers/google/keychain', () => ({ storeGoogleCalendarRefreshToken: storeGoogleCalendarRefreshTokenMock })) @@ -117,19 +117,19 @@ vi.mock('./calendar/repositories/calendar-sources-repository', () => ({ upsertCalendarSource: upsertCalendarSourceMock })) -vi.mock('./calendar/google/calendar-google-settings', () => ({ +vi.mock('./calendar/providers/google/calendar-google-settings', () => ({ writeCalendarGoogleSettings: writeCalendarGoogleSettingsMock })) -vi.mock('./calendar/google/push-runtime', () => ({ +vi.mock('./calendar/providers/google/push-runtime', () => ({ getGooglePushRuntime: getGooglePushRuntimeMock })) -vi.mock('./calendar/google/google-sync-runner', () => ({ +vi.mock('./calendar/providers/google/google-sync-runner', () => ({ startGoogleCalendarSyncRunner: startGoogleCalendarSyncRunnerMock })) -vi.mock('./calendar/google/sync-service', () => ({ +vi.mock('./calendar/providers/google/sync-service', () => ({ syncGoogleCalendarSource: syncGoogleCalendarSourceMock, pushSourceToGoogleCalendar: pushSourceToGoogleCalendarMock })) diff --git a/apps/desktop/src/main/test-hooks.ts b/apps/desktop/src/main/test-hooks.ts index 17878f5ce..dd0975b82 100644 --- a/apps/desktop/src/main/test-hooks.ts +++ b/apps/desktop/src/main/test-hooks.ts @@ -11,15 +11,15 @@ import { getDatabase } from './database' import { sql } from 'drizzle-orm' import { getNoteMetadataById } from '@memry/storage-data' import { CalendarChannels, TasksChannels } from '@memry/contracts/ipc-channels' -import { storeGoogleCalendarRefreshToken } from './calendar/google/keychain' +import { storeGoogleCalendarRefreshToken } from './calendar/providers/google/keychain' import { upsertCalendarSource } from './calendar/repositories/calendar-sources-repository' -import { writeCalendarGoogleSettings } from './calendar/google/calendar-google-settings' -import { getGooglePushRuntime } from './calendar/google/push-runtime' -import { startGoogleCalendarSyncRunner } from './calendar/google/google-sync-runner' +import { writeCalendarGoogleSettings } from './calendar/providers/google/calendar-google-settings' +import { getGooglePushRuntime } from './calendar/providers/google/push-runtime' +import { startGoogleCalendarSyncRunner } from './calendar/providers/google/google-sync-runner' import { pushSourceToGoogleCalendar, syncGoogleCalendarSource -} from './calendar/google/sync-service' +} from './calendar/providers/google/sync-service' import { listCalendarExternalEventsBySource } from './calendar/repositories/calendar-external-events-repository' import { calendarEvents } from '@memry/db-schema/schema/calendar-events' import { getMainI18n } from './lib/main-i18n' diff --git a/apps/desktop/src/main/zero-runtime-effects.test.ts b/apps/desktop/src/main/zero-runtime-effects.test.ts index 6ce7c6c64..b64abb428 100644 --- a/apps/desktop/src/main/zero-runtime-effects.test.ts +++ b/apps/desktop/src/main/zero-runtime-effects.test.ts @@ -116,7 +116,7 @@ vi.mock('./calendar/change-events', () => ({ emitCalendarProjectionChanged: mocks.emitCalendarProjectionChanged })) -vi.mock('./calendar/google/local-sync-effects', () => ({ +vi.mock('./calendar/providers/google/local-sync-effects', () => ({ scheduleGoogleCalendarSourceSync: mocks.scheduleGoogleCalendarSourceSync })) diff --git a/apps/desktop/tsconfig.test.node.json b/apps/desktop/tsconfig.test.node.json index 2ccac5488..0f52c147c 100644 --- a/apps/desktop/tsconfig.test.node.json +++ b/apps/desktop/tsconfig.test.node.json @@ -45,10 +45,10 @@ "src/main/ai-inline/ai-chat-server.test.ts", "src/main/ai-inline/ai-llm-service.test.ts", "src/main/billing/paddle-billing.activation.test.ts", - "src/main/calendar/google/account-routing.test.ts", - "src/main/calendar/google/calendar-source-discovery.test.ts", - "src/main/calendar/google/google-channel-manager.test.ts", - "src/main/calendar/google/sync-service.test.ts", + "src/main/calendar/providers/google/account-routing.test.ts", + "src/main/calendar/providers/google/calendar-source-discovery.test.ts", + "src/main/calendar/providers/google/google-channel-manager.test.ts", + "src/main/calendar/providers/google/sync-service.test.ts", "src/main/calendar/promote-external-event.test.ts", "src/main/calendar/repositories/calendar-events-repository.test.ts", "src/main/calendar/repositories/calendar-external-events-repository.test.ts", diff --git a/packages/contracts/src/sync-payloads.ts b/packages/contracts/src/sync-payloads.ts index d59bbbabd..af1103db3 100644 --- a/packages/contracts/src/sync-payloads.ts +++ b/packages/contracts/src/sync-payloads.ts @@ -291,14 +291,14 @@ export const CalendarEventSyncPayloadSchema = z.object({ colorId: z.string().nullable().optional(), conferenceData: z.record(z.string(), z.unknown()).nullable().optional(), // Recurrence-exception identity. Written locally by the Google writeback - // (calendar/google/sync-service.ts applyGoogleCalendarWriteback) and read back + // (calendar/providers/google/sync-service.ts applyGoogleCalendarWriteback) and read back // out by mapCalendarEventToGoogleInput to set recurringEventId/originalStartTime // on the next push. Without these keys zod stripped them on arrival, so a peer // re-pushed an exception as a brand new standalone event. parentEventId: z.string().nullable().optional(), originalStartTime: z.string().nullable().optional(), // Which remote calendar this event is pinned to. Consumed by - // calendar/google/account-routing.ts and sync-service.ts to route the push, and + // calendar/providers/google/account-routing.ts and sync-service.ts to route the push, and // set by promote-external-event.ts. Zod used to strip it on arrival, so the // receiving device silently fell back to the memry-managed calendar. // diff --git a/scripts/check-staged-secrets.mjs b/scripts/check-staged-secrets.mjs index 41cd7aea2..5f4c72d10 100644 --- a/scripts/check-staged-secrets.mjs +++ b/scripts/check-staged-secrets.mjs @@ -144,6 +144,13 @@ function isCodeDeclarationValue(filePath, value) { // the value is a function, not a literal. Quote characters anywhere in it // keep an embedded string literal flagged. /^\([^)'"`]*\)\s*=>[^'"`]*$/.test(normalized) || + // arrow function whose body is a call expression + // (`hashToken: async (plaintext) => createHmac('sha256', key).update(plaintext)`). + // The rule above rejects any quote so an embedded literal stays flagged, + // which also rejects the string arguments of a perfectly ordinary call. A + // callee name followed by `(` is the discriminator: a bare literal body + // like `() => 'sk-live-…'` has no callee and still fails. + /^(?:async\s*)?\([^)'"`]*\)\s*=>\s*[A-Za-z_$][\w$.]*\(/.test(normalized) || /^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$/i.test(normalized) || isSourceCodeReferenceValue(filePath, value) ) diff --git a/scripts/check-staged-secrets.test.mjs b/scripts/check-staged-secrets.test.mjs index 282cadae2..9d891d93c 100644 --- a/scripts/check-staged-secrets.test.mjs +++ b/scripts/check-staged-secrets.test.mjs @@ -54,6 +54,21 @@ describe('check-staged-secrets function values', () => { 'high-risk-secret-assignment' ]) }) + + it('ignores an async arrow whose body is a call with string arguments', () => { + assert.deepEqual( + rules( + " hashToken: async (plaintext) => createHmac('sha256', hmacKey).update(plaintext).digest('hex')," + ), + [] + ) + }) + + it('still flags an async arrow returning a bare string literal', () => { + assert.deepEqual(rules(" hashToken: async () => 'hunter2secretvalue'"), [ + 'high-risk-secret-assignment' + ]) + }) }) describe('check-staged-secrets keyword word boundaries', () => {