@cosmoops/connect-sdk 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ import type { BookableEventType, Booking, BookingAttendee } from "../types.js";
2
+ /**
3
+ * The bridge's scheduling surface, as hooks.
4
+ *
5
+ * Every path here is one of the six the bridge exposes, and every one of them
6
+ * is resolved *for the signed-in member* - Connect takes the workspace from
7
+ * their session and checks their own `calendar:read` / `calendar:manage`.
8
+ * Nothing in this file names a workspace, and nothing should: a browser that
9
+ * can name a tenant is a browser that can read someone else's diary.
10
+ */
11
+ /** What this workspace offers as bookable. Active types only. */
12
+ export declare function useBookableEventTypes(): {
13
+ eventTypes: BookableEventType[];
14
+ isLoading: boolean;
15
+ error: string | null;
16
+ };
17
+ export interface SlotQuery {
18
+ eventTypeId: string;
19
+ from: Date;
20
+ to: Date;
21
+ /**
22
+ * Set to the booking being moved. Without it the appointment counts as
23
+ * blocking its own replacement and a reschedule picker comes back empty for
24
+ * the one adjustment people actually make.
25
+ */
26
+ excludeBookingId?: string;
27
+ }
28
+ /**
29
+ * Free times for one appointment type.
30
+ *
31
+ * **Deliberately re-read rather than cached across a session.** A slot list is
32
+ * a snapshot of contention: anyone else on that calendar - a colleague, a
33
+ * voice agent mid-call, someone on the public booking link - can take one
34
+ * between the read and the click. Showing a stale one is how two people are
35
+ * offered the same minute. The booking call re-checks inside a transaction
36
+ * anyway, so a stale list costs a refusal rather than a double booking; this
37
+ * just means the refusal is rare.
38
+ */
39
+ export declare function useAvailableSlots(query: SlotQuery | null): {
40
+ slots: import("./index.js").BookingSlot[];
41
+ timezone: string | null;
42
+ durationMinutes: number | null;
43
+ isLoading: boolean;
44
+ error: string | null;
45
+ refetch: () => void;
46
+ };
47
+ export interface BookingsQuery {
48
+ from: Date;
49
+ to: Date;
50
+ /** Only appointments this member is hosting. Omit for the whole workspace. */
51
+ hostMemberId?: string;
52
+ limit?: number;
53
+ }
54
+ /** One page of the diary, ascending - a forward view, not a history. */
55
+ export declare function useBookings(query: BookingsQuery): {
56
+ bookings: Booking[];
57
+ hasMore: boolean;
58
+ isLoading: boolean;
59
+ error: string | null;
60
+ path: string;
61
+ refetch: () => void;
62
+ };
63
+ export interface BookingActionsState {
64
+ isPending: boolean;
65
+ /** The last refusal, written to be shown to the person who clicked. */
66
+ error: string | null;
67
+ clearError(): void;
68
+ }
69
+ /**
70
+ * Book, move and cancel.
71
+ *
72
+ * **None of these is optimistic**, and that is not an oversight. Whether a
73
+ * slot is still free is a question only the server can answer, inside the
74
+ * transaction that decides it - two people can be holding the same minute
75
+ * right now. Painting a booking as made and withdrawing it is exactly the lie
76
+ * the two-phase hold exists to prevent.
77
+ */
78
+ export declare function useBookingActions(): BookingActionsState & {
79
+ createBooking(input: {
80
+ eventTypeId: string;
81
+ startAt: string;
82
+ attendee: BookingAttendee;
83
+ notes?: string | null;
84
+ }): Promise<Booking | null>;
85
+ rescheduleBooking(input: {
86
+ bookingId: string;
87
+ startAt: string;
88
+ }): Promise<Booking | null>;
89
+ cancelBooking(input: {
90
+ bookingId: string;
91
+ reason?: string | null;
92
+ }): Promise<Booking | null>;
93
+ };
@@ -4,6 +4,7 @@ import { AppsResource } from "./resources/apps.js";
4
4
  import { AiResource } from "./resources/ai.js";
5
5
  import { DataResource } from "./resources/data.js";
6
6
  import { WorkspacesResource } from "./resources/workspaces.js";
7
+ import { SchedulingResource } from "./resources/scheduling.js";
7
8
  import { KvResource } from "./resources/kv.js";
8
9
  import { BillingResource } from "./resources/billing.js";
9
10
  import { DriveResource } from "./resources/drive.js";
@@ -75,6 +76,8 @@ export declare class ConnectClient {
75
76
  readonly billing: BillingResource;
76
77
  /** Files in a workspace's Drive. */
77
78
  readonly drive: DriveResource;
79
+ /** A subscribing workspace's own calendar - what is bookable, free, and booked. */
80
+ readonly scheduling: SchedulingResource;
78
81
  /** RAG chat and structured extraction. */
79
82
  readonly ai: AiResource;
80
83
  /** Your application's own document store - see `DataResource`. */
@@ -109,4 +112,4 @@ export declare class ConnectClient {
109
112
  constructor(options: ConnectClientOptions);
110
113
  }
111
114
  /** Kept in step with `package.json` by `sdk.test.ts`. */
112
- export declare const SDK_VERSION = "0.16.1";
115
+ export declare const SDK_VERSION = "0.18.0";
@@ -27,6 +27,7 @@ export type { AnalyticsResource } from "./resources/analytics.js";
27
27
  export type { SessionsResource } from "./resources/sessions.js";
28
28
  export type { ExecuteIntegrationActionInput, IntegrationsResource } from "./resources/integrations.js";
29
29
  export type { PlatformResource } from "./resources/platform.js";
30
+ export type { SchedulingResource, FindSlotsInput, ListBookingsInput, CreateBookingInput, RescheduleBookingInput, } from "./resources/scheduling.js";
30
31
  /**
31
32
  * Webhook verification and a ready-made POST route. Server-only for the
32
33
  * obvious reason: the signing secret must never reach a browser bundle.
@@ -0,0 +1,136 @@
1
+ import { type TransportOptions } from "../http.js";
2
+ import type { BookableEventType, Booking, RescheduledBooking, BookingAttendee, BookingSlots, BookingStatus, ListBookingsPage } from "../../types.js";
3
+ /**
4
+ * Present on every method here.
5
+ *
6
+ * With `asUser`, Connect derives the workspace from **that member's session**
7
+ * and ignores `workspaceId`, then checks their own `calendar:read` /
8
+ * `calendar:manage`. Without it the call is your server acting for a workspace
9
+ * you name, gated only on your application being installed there. The bridge
10
+ * always sends it - a browser must never be trusted to name a tenant, and an
11
+ * end user of your app is not automatically allowed to book on their
12
+ * employer's calendar.
13
+ */
14
+ export interface ActingAsOption {
15
+ /** The signed-in member's opaque Connect session token. */
16
+ asUser?: string;
17
+ }
18
+ export interface FindSlotsInput extends ActingAsOption {
19
+ workspaceId: string;
20
+ eventTypeId: string;
21
+ /** ISO-8601 instant, or a `Date`. */
22
+ from: string | Date;
23
+ to: string | Date;
24
+ /**
25
+ * A booking to treat as if it were not there - **set this when offering
26
+ * times to move an existing appointment to.**
27
+ *
28
+ * Without it the appointment counts as blocking its own replacement, so the
29
+ * adjustment people actually make - same day, half an hour later - comes
30
+ * back with nothing available.
31
+ */
32
+ excludeBookingId?: string;
33
+ }
34
+ export interface RescheduleBookingInput extends ActingAsOption {
35
+ workspaceId: string;
36
+ bookingId: string;
37
+ /** The new time, from a `findSlots` result, passed back unchanged. */
38
+ startAt: string | Date;
39
+ /** Replaces the original note when given; it carries over otherwise. */
40
+ notes?: string | null;
41
+ }
42
+ export interface ListBookingsInput extends ActingAsOption {
43
+ workspaceId: string;
44
+ status?: BookingStatus;
45
+ /** A Connect workspace member id - only bookings that committed them. */
46
+ hostMemberId?: string;
47
+ from?: string | Date;
48
+ to?: string | Date;
49
+ /** `asc` for a forward diary, `desc` (the default) for a history. */
50
+ order?: "asc" | "desc";
51
+ cursor?: string;
52
+ limit?: number;
53
+ }
54
+ export interface CreateBookingInput extends ActingAsOption {
55
+ workspaceId: string;
56
+ eventTypeId: string;
57
+ /**
58
+ * The `startAt` from a `findSlots` result, passed back unchanged.
59
+ *
60
+ * Re-deriving it from a local wall time is how an integration books an hour
61
+ * that was never offered - the slot list is computed in the *business's*
62
+ * timezone, which is rarely the one your process is running in.
63
+ */
64
+ startAt: string | Date;
65
+ attendee: BookingAttendee;
66
+ notes?: string | null;
67
+ }
68
+ /**
69
+ * A subscribing workspace's own calendar.
70
+ *
71
+ * **There is one calendar, not one per application.** A booking made here
72
+ * appears on the workspace's own screen immediately, and a slot their
73
+ * receptionist just took is gone from your next `findSlots`. That is the
74
+ * reason to read it through Connect rather than keeping a copy: two calendars
75
+ * double-book, and nothing reconciles them afterwards.
76
+ *
77
+ * Every method needs a live subscription to your application in the workspace
78
+ * you name. The key says which application is calling; the subscription is
79
+ * what makes you welcome in a given tenant.
80
+ */
81
+ export declare class SchedulingResource {
82
+ private readonly transport;
83
+ constructor(transport: TransportOptions);
84
+ /** What this workspace offers as bookable. Active appointment types only. */
85
+ listEventTypes(workspaceId: string, options?: ActingAsOption): Promise<BookableEventType[]>;
86
+ /**
87
+ * Free times for one appointment type over one window.
88
+ *
89
+ * **Advisory by construction.** Between this answer and `createBooking`,
90
+ * anyone else on that calendar can take any of these - a member, a voice
91
+ * agent mid-call, someone on the workspace's public booking link. That is
92
+ * why `createBooking` can answer `409`, and why that is an ordinary outcome
93
+ * rather than a fault.
94
+ *
95
+ * The window is clamped silently to now and to the appointment type's own
96
+ * booking horizon, so asking for a year of a sixty-day type returns sixty
97
+ * days rather than an error.
98
+ */
99
+ findSlots(input: FindSlotsInput): Promise<BookingSlots>;
100
+ /** One page of the workspace's diary. */
101
+ listBookings(input: ListBookingsInput): Promise<ListBookingsPage>;
102
+ /**
103
+ * Takes one of the times `findSlots` offered.
104
+ *
105
+ * Holds and confirms in one call - you already have the attendee, so there
106
+ * is no half-finished state for your integration to remember. The slot is
107
+ * re-checked inside a transaction, so a time taken since you read it throws
108
+ * a `ConnectError` with `code: "conflict"` rather than double-booking.
109
+ */
110
+ createBooking(input: CreateBookingInput): Promise<Booking>;
111
+ /**
112
+ * Moves an appointment to a different time.
113
+ *
114
+ * **The original is retired, not edited.** It stays on the diary as
115
+ * `rescheduled` with `rescheduledToId` pointing at the replacement, and both
116
+ * come back here - so a booking id you cached still resolves, and still says
117
+ * where the appointment went.
118
+ *
119
+ * A refused move changes nothing: the new slot is claimed before the old one
120
+ * is released, so a `conflict` leaves the customer with the appointment they
121
+ * already had. Re-read the slots (with `excludeBookingId`) and offer another.
122
+ */
123
+ rescheduleBooking(input: RescheduleBookingInput): Promise<RescheduledBooking>;
124
+ /**
125
+ * Frees the slot and records why.
126
+ *
127
+ * Not a deletion: the booking stays on the diary as `cancelled` with the
128
+ * reason, which is what makes a late cancellation something anyone can look
129
+ * up afterwards.
130
+ */
131
+ cancelBooking(input: ActingAsOption & {
132
+ workspaceId: string;
133
+ bookingId: string;
134
+ reason?: string | null;
135
+ }): Promise<Booking>;
136
+ }
@@ -1140,3 +1140,87 @@ export interface ConnectWorkspaceMember {
1140
1140
  granted: string[];
1141
1141
  role: string | null;
1142
1142
  }
1143
+ /**
1144
+ * One bookable thing a workspace offers - "15-minute intro call", "site
1145
+ * survey".
1146
+ *
1147
+ * `locationKind` and never the location itself: the address or the meeting
1148
+ * link belongs to whoever booked, and this is a catalogue.
1149
+ */
1150
+ export interface BookableEventType {
1151
+ id: string;
1152
+ /** Stable and readable, unique within the workspace - branch on this, not `id`. */
1153
+ slug: string;
1154
+ name: string;
1155
+ description: string | null;
1156
+ durationMinutes: number;
1157
+ locationKind: "phone" | "address" | "link";
1158
+ }
1159
+ export interface BookingSlot {
1160
+ /** ISO-8601 instant. Pass this back to `createBooking` unchanged. */
1161
+ startAt: string;
1162
+ endAt: string;
1163
+ }
1164
+ export interface BookingSlots {
1165
+ /**
1166
+ * The **business's** IANA timezone, not yours and not your user's - what to
1167
+ * render a slot's wall time in. A slot shown in the viewer's zone is an hour
1168
+ * the business may not be open.
1169
+ */
1170
+ timezone: string;
1171
+ durationMinutes: number;
1172
+ slots: BookingSlot[];
1173
+ }
1174
+ export interface BookingAttendee {
1175
+ name: string;
1176
+ phone: string;
1177
+ email?: string | null;
1178
+ }
1179
+ export type BookingStatus = "hold" | "confirmed" | "cancelled" | "rescheduled" | "no_show";
1180
+ /**
1181
+ * Where a booking came from - usually the first question anyone asks about an
1182
+ * appointment they did not expect.
1183
+ *
1184
+ * `api` is your integration; `portal` is the workspace's own screen or its
1185
+ * public booking link; `agent-call` is a voice agent, mid-call.
1186
+ */
1187
+ export type BookingSource = "agent-call" | "portal" | "api";
1188
+ export interface Booking {
1189
+ id: string;
1190
+ eventTypeId: string;
1191
+ status: BookingStatus;
1192
+ /** ISO-8601 instants. */
1193
+ startAt: string;
1194
+ endAt: string;
1195
+ /** The zone the two parties agreed the time in, frozen at booking. */
1196
+ timezone: string;
1197
+ attendee: {
1198
+ name: string;
1199
+ phone: string;
1200
+ email: string | null;
1201
+ };
1202
+ notes: string | null;
1203
+ source: BookingSource;
1204
+ cancelledReason: string | null;
1205
+ /**
1206
+ * Set when `status` is `rescheduled` - the booking that replaced this one.
1207
+ *
1208
+ * Rescheduling retires a booking rather than moving its `startAt`, so an id
1209
+ * you cached still resolves and still tells you where the appointment went.
1210
+ */
1211
+ rescheduledToId: string | null;
1212
+ createdAt: string;
1213
+ }
1214
+ export interface RescheduledBooking {
1215
+ /** The new appointment. This is the live one. */
1216
+ booking: Booking;
1217
+ /** The retired original, now `rescheduled` and pointing at `booking`. */
1218
+ previous: Booking;
1219
+ }
1220
+ export interface ListBookingsPage {
1221
+ bookings: Booking[];
1222
+ /** Pass back as `cursor`. A cursor belongs to one set of filters. */
1223
+ nextCursor: string | null;
1224
+ hasMore: boolean;
1225
+ totalCount: number;
1226
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",