Write a provider
Add a calendar calsdk does not support yet, with the same contract, helpers and tests as the official providers.
A provider is a function that returns a plain object implementing CalendarProviderV1 from @cal-sdk/provider. Its job is narrow on purpose: translate calsdk's requests into one API's requests, and the responses back into calsdk's shapes. What would be the same for every calendar lives in @cal-sdk/core instead (validation, retries, pagination loops, the free/busy fallback and warning logging), so you only write the part that's specific to your API.
npm install @cal-sdk/provider @cal-sdk/provider-utils zod
npm install -D @cal-sdk/provider-testsThe official providers are built on exactly these packages, so yours gets the same tools.
The contract
Here's an outline of the type. The package has the full option and result types for each method.
type CalendarProviderV1<Provider extends string, Types extends ProviderTypesV1> = {
readonly specificationVersion: 'v1';
readonly provider: Provider; // also the key in providerOptions and providerMetadata
readonly capabilities: CalendarCapabilitiesV1;
doListCalendars(options): PromiseLike<{ calendars; warnings }>;
doGetEvent(options): PromiseLike<{ event; warnings }>;
doListEvents(options): PromiseLike<{ events; nextPageToken?; warnings }>;
doCreateEvent(options): PromiseLike<{ event; warnings }>;
doUpdateEvent(options): PromiseLike<{ event; warnings }>;
doDeleteEvent(options): PromiseLike<{ warnings }>;
doFreeBusy?(options): PromiseLike<{ calendars; warnings }>; // calsdk falls back to doListEvents
doSyncEvents?(options): PromiseLike<{ changes; nextPageToken?; nextCursor?; warnings }>;
};The types catch shape mistakes. These rules cover what they can't:
- Declare capabilities honestly. calsdk picks fallbacks from them, and the conformance suite only tests what you declare as
true. - Resolve
'primary'to the user's default calendar. - Keep progress in cursors. Calls may run at once from many places, so never keep sync position or other per-call state in the provider. Caching setup that doesn't change, such as CalDAV's server discovery, is fine.
- Return exact times. Timed
dateTimes carry an offset, andtimeZoneis an IANA name, so every reader gets the same instant. - Map errors to calsdk's classes, so apps can catch
NotFoundErrororConflictErrorthe same way for every calendar.mapAPICallErrordoes this for HTTP status codes. - Never save something else. If the API can't store what was asked, throw
UnsupportedFunctionalityError: a meeting on the wrong day is worse than an error. If you drop a harmless part, such as a color, return anunsupportedwarning instead. - Pass
abortSignalandheadersto every request, so callers can cancel a call and add their own headers. - Read only your own key in
providerOptions. Apps may pass options for several providers at once.
A minimal provider
import {
type CalendarProviderV1,
type EventV1,
UnsupportedFunctionalityError,
} from '@cal-sdk/provider';
import {
type AccessToken,
createJsonFetcher,
type FetchFunction,
mapAPICallError,
urlPath,
} from '@cal-sdk/provider-utils';
import { z } from 'zod';
const eventSchema = z.object({
id: z.string(),
name: z.string(),
starts: z.string(),
ends: z.string(),
zone: z.string(),
});
function fromAcme(raw: z.infer<typeof eventSchema>, calendarId: string): EventV1 {
return {
id: raw.id,
calendarId,
kind: 'single',
title: raw.name,
start: { dateTime: raw.starts, timeZone: raw.zone },
end: { dateTime: raw.ends, timeZone: raw.zone },
status: 'confirmed',
showAs: 'busy',
};
}
type AcmeSettings = { accessToken: AccessToken; fetch?: FetchFunction };
export function acme(settings: AcmeSettings): CalendarProviderV1<'acme'> {
const request = createJsonFetcher({ baseURL: 'https://api.acme.example/v1', ...settings });
return {
specificationVersion: 'v1',
provider: 'acme',
capabilities: {
events: {
details: true,
allDay: false,
recurrence: false,
recurrenceScopeThisAndFollowing: false,
serverSideExpansion: false,
attendees: false,
conference: false,
reminders: false,
color: false,
etag: false,
search: false,
},
freeBusy: false,
sync: false,
webhooks: false,
},
async doGetEvent({ calendarId, eventId, abortSignal, headers }) {
try {
const { value } = await request({
method: 'GET',
path: urlPath`/calendars/${calendarId}/events/${eventId}`,
schema: eventSchema,
abortSignal,
headers,
});
return { event: fromAcme(value, calendarId), warnings: [] };
} catch (error) {
throw mapAPICallError(error, { provider: 'acme', resource: { type: 'event', id: eventId } });
}
},
async doCreateEvent({ calendarId, event }) {
if (event.recurrence) {
throw new UnsupportedFunctionalityError({ provider: 'acme', functionality: 'recurring events' });
}
// ...
},
// doListCalendars, doListEvents, doUpdateEvent, doDeleteEvent
};
}createJsonFetcher sends the bearer token, resolving it per request, and validates responses with any Standard Schema library such as zod. It never follows redirects and never puts request headers in its errors, so the token can't leak to another host or into your logs. urlPath encodes ids, which can contain @, / and #.
Type your options and metadata
Declare what each operation reads from providerOptions, and what you put in providerMetadata, and calsdk type-checks them against the provider passed as calendar:
type AcmeTypes = {
options: { createEvent: { notifyRoom?: boolean } };
eventMetadata: { roomId?: string };
};
export function acme(settings: AcmeSettings): CalendarProviderV1<'acme', AcmeTypes> {
// ...
}Validate the options at runtime too, since callers may not use TypeScript. parseProviderOptions reads your key, throws InvalidArgumentError for wrong values and warns about unknown keys:
const { options, warnings } = await parseProviderOptions({
provider: 'acme',
providerOptions,
schema: z.object({ notifyRoom: z.boolean().optional() }),
});Next
Test it
Run the conformance suite the official providers pass.
Helpers
Fetching, errors, options, time zones and recurrence.
When you're unsure what a method should return, read the mock provider. It implements every operation in memory, with no API in the way, and is the clearest reference.