/** * CalendarClient interface + thin implementation. * * The real implementation calls the Google Calendar API v3 using a per-user * OAuth2 access token supplied by the injected token provider. The actual * googleapis SDK call is clearly stubbed — see the TODO below — so this file * compiles and is safe to import without any network activity or AWS calls. * * Tests inject a MockCalendarClient that implements the same interface. */ // --------------------------------------------------------------------------- // Domain types // --------------------------------------------------------------------------- export interface CalendarEvent { id: string; summary: string; description?: string; start: string; // ISO-8601 datetime or date end: string; // ISO-8601 datetime or date attendees?: CalendarAttendee[]; /** True when at least one attendee is not @seahavenind.com */ hasExternalAttendees?: boolean; status?: string; // confirmed | tentative | cancelled htmlLink?: string; } export interface CalendarAttendee { email: string; displayName?: string; responseStatus?: string; // accepted | declined | needsAction | tentative organizer?: boolean; self?: boolean; } export interface GetEventsOptions { calendarId?: string; // defaults to 'primary' timeMin: string; // ISO-8601 timeMax: string; // ISO-8601 maxResults?: number; singleEvents?: boolean; orderBy?: 'startTime' | 'updated'; } export interface CheckAvailabilityOptions { /** ISO-8601 start of window to check */ timeMin: string; /** ISO-8601 end of window to check */ timeMax: string; /** Defaults to 'primary' */ calendarId?: string; } export interface AvailabilityResult { busy: Array<{ start: string; end: string }>; free: Array<{ start: string; end: string }>; } export interface CreateEventOptions { calendarId?: string; // defaults to 'primary' summary: string; description?: string; start: string; // ISO-8601 datetime end: string; // ISO-8601 datetime attendees?: Array<{ email: string; displayName?: string }>; location?: string; timeZone?: string; } // --------------------------------------------------------------------------- // Interface — every consumer codes against this, never against the concrete impl // --------------------------------------------------------------------------- export interface CalendarClient { /** * Returns events from the user's calendar in the requested window. * Throws CalendarClientError on API errors. */ getEvents(userSub: string, opts: GetEventsOptions): Promise; /** * Queries free/busy information for the user's calendar. */ checkAvailability( userSub: string, opts: CheckAvailabilityOptions, ): Promise; /** * Creates a calendar event and returns the created event. * Callers MUST inspect hasExternalAttendees on the result and surface the * warning to the user (design §2.5 — outbound invite monitoring). */ createEvent(userSub: string, opts: CreateEventOptions): Promise; } // --------------------------------------------------------------------------- // Error type // --------------------------------------------------------------------------- export class CalendarClientError extends Error { constructor( message: string, public readonly statusCode?: number, public readonly retryable: boolean = false, ) { super(message); this.name = 'CalendarClientError'; } } // --------------------------------------------------------------------------- // Per-user token provider interface // // Real implementation fetches/refreshes the per-user Google OAuth2 refresh // token from KMS-CMK-encrypted DynamoDB (keyed by user `sub`, ABAC-partitioned // by LeadingKeys — design §2.4). This interface is injected so tests can mock // it without any AWS/DDB calls at import time. // --------------------------------------------------------------------------- export interface GoogleTokenProvider { /** * Returns a short-lived Google OAuth2 access token scoped to ONLY * `https://www.googleapis.com/auth/calendar` for the given user. * * The returned token is minted per-request and is never cached by this * interface (design §2.4 — access tokens not cached or reused). * * Throws if the user has not yet granted the Calendar OAuth consent. */ getAccessToken(userSub: string): Promise; } // --------------------------------------------------------------------------- // Concrete implementation (real call stubbed — see TODO) // --------------------------------------------------------------------------- /** * Thin wrapper around the Google Calendar API v3. * * Construction is cheap (no network, no AWS) — the tokenProvider handles all * credential retrieval lazily at call time. */ export class GoogleCalendarClient implements CalendarClient { // Stored for use by the real implementation once the token store is available (design §2.4). private readonly _tokenProvider: GoogleTokenProvider; constructor(tokenProvider: GoogleTokenProvider) { this._tokenProvider = tokenProvider; // Mark as intentionally stored-but-unused until the real SDK call is wired. void this._tokenProvider; } async getEvents(userSub: string, opts: GetEventsOptions): Promise { // TODO: replace the stub below with the real googleapis SDK call. // // import { google } from 'googleapis'; // const accessToken = await this.tokenProvider.getAccessToken(userSub); // const auth = new google.auth.OAuth2(); // auth.setCredentials({ access_token: accessToken }); // const cal = google.calendar({ version: 'v3', auth }); // const res = await cal.events.list({ // calendarId: opts.calendarId ?? 'primary', // timeMin: opts.timeMin, // timeMax: opts.timeMax, // maxResults: opts.maxResults ?? 50, // singleEvents: opts.singleEvents ?? true, // orderBy: opts.orderBy ?? 'startTime', // }); // return (res.data.items ?? []).map(mapEvent); // // DEFERRED: awaiting the 0a-gated per-user token + DDB store (design §2.4). void userSub; void opts; throw new CalendarClientError( 'GoogleCalendarClient.getEvents is not yet implemented — awaiting per-user token store (design §2.4)', 501, false, ); } async checkAvailability( userSub: string, opts: CheckAvailabilityOptions, ): Promise { // TODO: replace the stub below with the real googleapis SDK call. // // import { google } from 'googleapis'; // const accessToken = await this.tokenProvider.getAccessToken(userSub); // const auth = new google.auth.OAuth2(); // auth.setCredentials({ access_token: accessToken }); // const cal = google.calendar({ version: 'v3', auth }); // const res = await cal.freebusy.query({ // requestBody: { // timeMin: opts.timeMin, // timeMax: opts.timeMax, // items: [{ id: opts.calendarId ?? 'primary' }], // }, // }); // return computeFreeBusy(opts.timeMin, opts.timeMax, res.data); // // DEFERRED: awaiting the 0a-gated per-user token + DDB store (design §2.4). void userSub; void opts; throw new CalendarClientError( 'GoogleCalendarClient.checkAvailability is not yet implemented — awaiting per-user token store (design §2.4)', 501, false, ); } async createEvent(userSub: string, opts: CreateEventOptions): Promise { // TODO: replace the stub below with the real googleapis SDK call. // // import { google } from 'googleapis'; // const accessToken = await this.tokenProvider.getAccessToken(userSub); // const auth = new google.auth.OAuth2(); // auth.setCredentials({ access_token: accessToken }); // const cal = google.calendar({ version: 'v3', auth }); // const res = await cal.events.insert({ // calendarId: opts.calendarId ?? 'primary', // requestBody: { // summary: opts.summary, // description: opts.description, // start: { dateTime: opts.start, timeZone: opts.timeZone }, // end: { dateTime: opts.end, timeZone: opts.timeZone }, // attendees: opts.attendees, // location: opts.location, // }, // }); // return mapEvent(res.data); // // DEFERRED: awaiting the 0a-gated per-user token + DDB store (design §2.4). void userSub; void opts; throw new CalendarClientError( 'GoogleCalendarClient.createEvent is not yet implemented — awaiting per-user token store (design §2.4)', 501, false, ); } }