@odla-ai/calendar
v0.1.2
Published
Provider-backed booking-mirror SDK for odla apps, with public embed config, verified attendee lookups, and timezone-safe helpers.
Maintainers
Readme
@odla-ai/calendar
Typed booking-mirror reads, public embed configuration, a reserved scheduling-action client surface, and browser-safe time formatting for odla apps. Google Calendar remains authoritative; the hosted read-only connector keeps odla's protected mirror synchronized.
npm i @odla-ai/calendarimport { initCalendar } from "@odla-ai/calendar";
const calendar = initCalendar({
appId: env.ODLA_TENANT,
adminToken: env.ODLA_API_KEY,
endpoint: env.ODLA_ENDPOINT,
});
const next = await calendar.bookings.next("[email protected]", {
from: Date.now(),
});Enable services: ["db", "calendar"] and configure calendar.google in
odla.config.mjs, including selected calendars per environment, optional
event-match filters, attendee policy, and the public Appointment Schedule URL.
npx odla-ai provision starts a human Google OAuth checkpoint; the CLI and app
never receive the provider token. Studio and the odla-ai calendar lifecycle
commands expose only owner-safe status and controls.
Studio stages a first connection safely: OAuth installs encrypted credentials
and calendar discovery, then reports needs_sync without mirroring the default
primary/full policy. The owner confirms calendars, filters, attendee policy,
and booking URL; Save settings starts initial sync. CLI provision already has
that intent checked in, so it can auto-sync after consent. Later semantic config
changes reuse the installed credential and resync rather than requesting OAuth.
Embed-mode browser code can load the intentionally public projection without an app key:
import { loadCalendarPublicConfig } from "@odla-ai/calendar/client";
const config = await loadCalendarPublicConfig({
endpoint: "https://odla.ai",
appId: "my-app",
env: "prod",
});
// { enabled, bookingPageUrl, connected, lastSuccessfulSyncAt }The root entry is for trusted backends only. It reuses the app's existing odla
API key to query the platform-owned $bookings mirror and send action requests;
provider credentials never enter the app. Browser code should read
$bookings with @odla-ai/db/client under explicit rules and import pure
helpers from @odla-ai/calendar/client.
Every mirrored booking has required bookingKey and syncedAt fields. Mirror
ingestion is limited to the machine-only apply route protected by the dedicated
CALENDAR_ADMIN_SECRET; each request must atomically provide a complete booking
projection and its exactly matching normalized attendee-index rows. ID-only
Google deletions are merged with the last row so cancellation remains a
queryable tombstone. The ingress also locks the tenant to its declared full
or hashed attendee policy; full projection updates clear optional fields that
are no longer present.
The hosted connector is deliberately read-only, so every action request still
returns a non-retryable HTTP 501 calendar_not_connected error. The action
methods below document the reserved contract for a future read/write slice.
Creation and mutation therefore happen in Google, and the mirror follows:
new bookings arrive through the public Appointment Schedule page, and
attendees reschedule or cancel through the links in Google's own
confirmation emails. To offer "reschedule"/"cancel" from your app UI today,
deep-link the organizer to the event via the booking's htmlLink field —
they change it in Google Calendar, and the mirror picks it up on the next
sync (push watch, or the 15-minute fallback). A cancelled event keeps its
row with status: "cancelled", so the app can react to cancellations too.
await calendar.actions.create({
calendarId: "primary",
summary: "Intro call",
startAt,
endAt,
attendees: ["[email protected]"],
}, { idempotencyKey: `application:${applicationId}:intro` });
await calendar.actions.reschedule(bookingId, { startAt, endAt, notify: true });
await calendar.actions.cancel(bookingId, { notify: true });Current boundary
- The shipped user experience is a public Google Appointment Schedule link or
embed plus read-only reconciliation. There is no native slot picker or
create/reschedule/cancel UI/API; in-app "manage this booking" affordances
should deep-link to Google via
htmlLinkas described above. - Full/recovery sync uses a fixed 365-day lookback, a 50-page provider budget
across the connection, and a 4,000-event scan cap per calendar. One
connection selects at most 10 calendars and preflights at most 400 booking
changes per run; larger scans fail with
calendar_sync_page_limit, while a larger change set fails before mirror writes withcalendar_sync_mutation_limit. Reconciliation tracks at most 15 distinct selected or historical calendar ids and materializes at most 20 actionable watch channels per connection. Google responses are restricted to the projection fields and at most 500 returned attendees; an omitted/truncated attendee set is never trusted, and mirrored events support at most 499 distinct attendees. The serialized connection-wide projection plan is also capped at 8 MB and fails early withcalendar_sync_projection_bytes_limit. Configurable rolling windows, continuation, and age-out are not implemented. - The hosted connector requires Workers Paid, or an equivalent runtime with at least 1,000 D1 queries per invocation. The Workers Free 50-query limit is not supported by this synchronization envelope.
- Google 429 responses fail the current sync run. There is no bounded in-run quota retry/backoff; a later scheduled or manual sync retries.
- The 15-minute fallback processes one oldest eligible connection per trigger so two worst-case syncs never share one D1 invocation budget. Webhook and manual syncs each run in their own invocation. Larger fleets need deferred Queue or Workflow fan-out instead of raising this v0.1 cron throughput.
- Booking rows and cancellation tombstones have no retention/purge workflow. Disconnect preserves potentially stale mirror rows.
- Disconnect stops watches and deletes this app/environment connection's encrypted platform token and cursors. It does not revoke the shared user-to-Google-OAuth-project grant, which may support other odla connections; a future global revoke would need cross-connection accounting and warning.
- Mirror apply batches use fresh random mutation ids, not provider-derived
(calendarId, eventId, event.updated)ids. - Public health is aggregate (
connectedandlastSuccessfulSyncAt); calendar ids remain owner-only, and per-calendar health is not published.
Connections require the platform Google OAuth/decryption/ingress secrets. A
fresh tenant that has never been connected has no mirror rows. If a tenant is
disconnected after data was mirrored, those rows remain readable and must be
treated as stale. Prefer a dedicated scheduling calendar; otherwise configure
conservative filters to avoid mirroring unrelated meetings. Attendee policy is
immutable after the first successful mirror apply.
Actions currently throw OdlaError with code === "calendar_not_connected",
status === 501, and retryable === false.
See the full package documentation
for mirror semantics, security boundaries, formatting, errors, and current
platform availability.
