@agent-native/scheduling 0.2.4 → 0.2.5-nightly-20260929152122

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.
Files changed (32) hide show
  1. package/dist/actions/assign-round-robin-host.js +3 -1
  2. package/dist/actions/assign-round-robin-host.js.map +1 -1
  3. package/dist/actions/create-booking.js +3 -3
  4. package/dist/actions/create-booking.js.map +1 -1
  5. package/dist/actions/submit-routing-form-response.js +4 -4
  6. package/dist/actions/submit-routing-form-response.js.map +1 -1
  7. package/dist/server/booking-service.d.ts.map +1 -1
  8. package/dist/server/booking-service.js +27 -20
  9. package/dist/server/booking-service.js.map +1 -1
  10. package/dist/server/bookings-repo.d.ts +1 -1
  11. package/dist/server/bookings-repo.d.ts.map +1 -1
  12. package/dist/server/bookings-repo.js +8 -3
  13. package/dist/server/bookings-repo.js.map +1 -1
  14. package/dist/server/event-types-repo.d.ts.map +1 -1
  15. package/dist/server/event-types-repo.js +6 -2
  16. package/dist/server/event-types-repo.js.map +1 -1
  17. package/dist/server/hooks.d.ts.map +1 -1
  18. package/dist/server/hooks.js +68 -20
  19. package/dist/server/hooks.js.map +1 -1
  20. package/dist/shared/index.d.ts +4 -0
  21. package/dist/shared/index.d.ts.map +1 -1
  22. package/package.json +4 -4
  23. package/src/actions/assign-round-robin-host.ts +8 -0
  24. package/src/actions/create-booking.ts +3 -4
  25. package/src/actions/event-type-authz.spec.ts +35 -0
  26. package/src/actions/submit-routing-form-response.ts +10 -4
  27. package/src/server/booking-service.spec.ts +458 -1
  28. package/src/server/booking-service.ts +27 -20
  29. package/src/server/bookings-repo.ts +21 -1
  30. package/src/server/event-types-repo.ts +16 -2
  31. package/src/server/hooks.ts +124 -23
  32. package/src/shared/index.ts +4 -0
@@ -1,27 +1,38 @@
1
- import { eq, and, inArray } from "drizzle-orm";
1
+ import { accessFilter } from "@agent-native/core/sharing";
2
+ import { and, eq, inArray, isNull, like, or, sql } from "drizzle-orm";
2
3
  import { nanoid } from "nanoid";
3
4
 
4
5
  import { addMinutes } from "../core/time.js";
5
6
  import type { Booking, Workflow, WorkflowStep } from "../shared/index.js";
6
7
  import { getSchedulingContext } from "./context.js";
7
8
 
9
+ interface BookingPrincipal {
10
+ ownerEmail: string;
11
+ orgId: string | null;
12
+ teamId: string | null;
13
+ }
14
+
8
15
  export async function onBookingCreated(booking: Booking): Promise<void> {
16
+ const principal = getBookingPrincipal(booking);
9
17
  await materializeReminders(booking, "new-booking");
10
18
  await materializeRemindersBefore(booking);
11
- await enqueueWebhooks(booking, "BOOKING_CREATED");
19
+ await enqueueWebhooks(booking, "BOOKING_CREATED", principal);
12
20
  }
13
21
 
14
22
  export async function onBookingRescheduled(
15
23
  _original: Booking,
16
24
  next: Booking,
17
25
  ): Promise<void> {
18
- await materializeReminders(next, "reschedule");
19
- await enqueueWebhooks(next, "BOOKING_RESCHEDULED");
26
+ const scopedBooking = await withBookingTeamScope(next);
27
+ const principal = getBookingPrincipal(scopedBooking);
28
+ await materializeReminders(scopedBooking, "reschedule");
29
+ await enqueueWebhooks(scopedBooking, "BOOKING_RESCHEDULED", principal);
20
30
  }
21
31
 
22
32
  export async function onBookingCancelled(booking: Booking): Promise<void> {
33
+ const principal = getBookingPrincipal(booking);
23
34
  await materializeReminders(booking, "cancellation");
24
- await enqueueWebhooks(booking, "BOOKING_CANCELLED");
35
+ await enqueueWebhooks(booking, "BOOKING_CANCELLED", principal);
25
36
  const { getDb, schema } = getSchedulingContext();
26
37
  await getDb()
27
38
  .update(schema.scheduledReminders)
@@ -35,15 +46,47 @@ export async function onBookingCancelled(booking: Booking): Promise<void> {
35
46
  }
36
47
 
37
48
  export async function onBookingNoShow(booking: Booking): Promise<void> {
38
- await materializeReminders(booking, "no-show");
39
- await enqueueWebhooks(booking, "BOOKING_NO_SHOW");
49
+ const scopedBooking = await withBookingTeamScope(booking);
50
+ const principal = getBookingPrincipal(scopedBooking);
51
+ await materializeReminders(scopedBooking, "no-show");
52
+ await enqueueWebhooks(scopedBooking, "BOOKING_NO_SHOW", principal);
53
+ }
54
+
55
+ async function withBookingTeamScope(booking: Booking): Promise<Booking> {
56
+ if (booking.teamId) return booking;
57
+ const { getDb, schema } = getSchedulingContext();
58
+ const principal = getBookingPrincipal(booking);
59
+ const [eventType] = await getDb()
60
+ .select({ teamId: schema.eventTypes.teamId })
61
+ .from(schema.eventTypes)
62
+ .where(
63
+ and(
64
+ eq(schema.eventTypes.id, booking.eventTypeId),
65
+ principal.orgId
66
+ ? or(
67
+ eq(schema.eventTypes.ownerEmail, principal.ownerEmail),
68
+ eq(schema.eventTypes.orgId, principal.orgId),
69
+ )
70
+ : eq(schema.eventTypes.ownerEmail, principal.ownerEmail),
71
+ ),
72
+ )
73
+ .limit(1);
74
+ return eventType?.teamId ? { ...booking, teamId: eventType.teamId } : booking;
75
+ }
76
+
77
+ function getBookingPrincipal(booking: Booking): BookingPrincipal {
78
+ return {
79
+ ownerEmail: (booking.ownerEmail ?? booking.hostEmail).trim().toLowerCase(),
80
+ orgId: booking.orgId ?? null,
81
+ teamId: booking.teamId ?? null,
82
+ };
40
83
  }
41
84
 
42
85
  async function materializeReminders(
43
86
  booking: Booking,
44
87
  trigger: "new-booking" | "reschedule" | "cancellation" | "no-show",
45
88
  ): Promise<void> {
46
- const workflows = await activeWorkflowsForEvent(booking.eventTypeId, trigger);
89
+ const workflows = await activeWorkflowsForEvent(booking, trigger);
47
90
  const now = new Date();
48
91
  for (const wf of workflows) {
49
92
  for (const step of wf.steps) {
@@ -57,10 +100,7 @@ async function materializeReminders(
57
100
  }
58
101
 
59
102
  async function materializeRemindersBefore(booking: Booking): Promise<void> {
60
- const workflows = await activeWorkflowsForEvent(
61
- booking.eventTypeId,
62
- "before-event",
63
- );
103
+ const workflows = await activeWorkflowsForEvent(booking, "before-event");
64
104
  const start = new Date(booking.startTime);
65
105
  for (const wf of workflows) {
66
106
  for (const step of wf.steps) {
@@ -69,10 +109,7 @@ async function materializeRemindersBefore(booking: Booking): Promise<void> {
69
109
  await writeReminder(booking, step, scheduledFor);
70
110
  }
71
111
  }
72
- const afterWorkflows = await activeWorkflowsForEvent(
73
- booking.eventTypeId,
74
- "after-event",
75
- );
112
+ const afterWorkflows = await activeWorkflowsForEvent(booking, "after-event");
76
113
  const end = new Date(booking.endTime);
77
114
  for (const wf of afterWorkflows) {
78
115
  for (const step of wf.steps) {
@@ -112,11 +149,21 @@ function methodForAction(
112
149
  }
113
150
 
114
151
  async function activeWorkflowsForEvent(
115
- eventTypeId: string,
152
+ booking: Booking,
116
153
  trigger: string,
117
154
  ): Promise<Workflow[]> {
118
155
  const { getDb, schema } = getSchedulingContext();
119
156
  const db = getDb();
157
+ const unteamedAccessScope = and(
158
+ isNull(schema.workflows.teamId),
159
+ accessFilter(schema.workflows, schema.workflowShares, {
160
+ userEmail: booking.ownerEmail ?? booking.hostEmail,
161
+ orgId: booking.orgId ?? undefined,
162
+ }),
163
+ );
164
+ const accessScope = booking.teamId
165
+ ? or(eq(schema.workflows.teamId, booking.teamId), unteamedAccessScope)
166
+ : unteamedAccessScope;
120
167
  const wfRows = await db
121
168
  .select()
122
169
  .from(schema.workflows)
@@ -124,11 +171,16 @@ async function activeWorkflowsForEvent(
124
171
  and(
125
172
  eq(schema.workflows.trigger, trigger as any),
126
173
  eq(schema.workflows.disabled, false),
174
+ like(
175
+ schema.workflows.activeOnEventTypeIds,
176
+ `%"${booking.eventTypeId}"%`,
177
+ ),
178
+ accessScope,
127
179
  ),
128
180
  );
129
181
  const active = wfRows.filter((w: any) => {
130
182
  const ids = safeJson<string[]>(w.activeOnEventTypeIds) ?? [];
131
- return ids.includes(eventTypeId);
183
+ return ids.includes(booking.eventTypeId);
132
184
  });
133
185
  if (active.length === 0) return [];
134
186
  const stepRows = await db
@@ -143,21 +195,70 @@ async function activeWorkflowsForEvent(
143
195
  return active.map((w: any) => hydrateWorkflow(w, stepRows));
144
196
  }
145
197
 
146
- async function enqueueWebhooks(booking: Booking, event: string): Promise<void> {
198
+ async function enqueueWebhooks(
199
+ booking: Booking,
200
+ event: string,
201
+ principal: BookingPrincipal,
202
+ ): Promise<void> {
147
203
  const { getDb, schema } = getSchedulingContext();
148
204
  const db = getDb();
149
- const webhooks = await db.select().from(schema.webhooks);
205
+ const teamScope = principal.teamId
206
+ ? or(
207
+ eq(schema.webhooks.teamId, principal.teamId),
208
+ isNull(schema.webhooks.teamId),
209
+ )
210
+ : isNull(schema.webhooks.teamId);
211
+ const webhookScopes = [
212
+ and(
213
+ sql`lower(${schema.webhooks.ownerEmail}) = ${principal.ownerEmail.trim().toLowerCase()}`,
214
+ principal.orgId
215
+ ? or(
216
+ eq(schema.webhooks.orgId, principal.orgId),
217
+ isNull(schema.webhooks.orgId),
218
+ )
219
+ : isNull(schema.webhooks.orgId),
220
+ teamScope,
221
+ )!,
222
+ ];
223
+ if (principal.orgId) {
224
+ webhookScopes.push(
225
+ and(
226
+ eq(schema.webhooks.orgId, principal.orgId),
227
+ eq(schema.webhooks.visibility, "org"),
228
+ teamScope,
229
+ )!,
230
+ );
231
+ }
232
+ const webhooks = await db
233
+ .select()
234
+ .from(schema.webhooks)
235
+ .where(
236
+ and(
237
+ eq(schema.webhooks.active, true),
238
+ or(...webhookScopes),
239
+ like(schema.webhooks.eventTriggers, `%"${event}"%`),
240
+ or(
241
+ isNull(schema.webhooks.eventTypeId),
242
+ eq(schema.webhooks.eventTypeId, booking.eventTypeId),
243
+ ),
244
+ ),
245
+ );
150
246
  const now = new Date().toISOString();
247
+ const {
248
+ cancelToken: _cancelToken,
249
+ rescheduleToken: _rescheduleToken,
250
+ ...webhookBooking
251
+ } = booking;
252
+ void _cancelToken;
253
+ void _rescheduleToken;
151
254
  for (const wh of webhooks) {
152
- if (!wh.active) continue;
153
255
  const triggers = safeJson<string[]>(wh.eventTriggers) ?? [];
154
256
  if (!triggers.includes(event)) continue;
155
- if (wh.eventTypeId && wh.eventTypeId !== booking.eventTypeId) continue;
156
257
  await db.insert(schema.webhookDeliveries).values({
157
258
  id: nanoid(),
158
259
  webhookId: wh.id,
159
260
  triggeredAt: now,
160
- payload: JSON.stringify({ event, booking }),
261
+ payload: JSON.stringify({ event, booking: webhookBooking }),
161
262
  success: false,
162
263
  attempts: 0,
163
264
  });
@@ -78,6 +78,7 @@ export interface EventType {
78
78
  position: number;
79
79
  schedulingType: SchedulingType;
80
80
  ownerEmail?: string;
81
+ orgId?: string;
81
82
  teamId?: string;
82
83
  scheduleId?: string;
83
84
  locations: Location[];
@@ -161,6 +162,9 @@ export interface Booking {
161
162
  uid: string;
162
163
  eventTypeId: string;
163
164
  hostEmail: string;
165
+ ownerEmail?: string;
166
+ orgId?: string | null;
167
+ teamId?: string;
164
168
  title: string;
165
169
  description?: string;
166
170
  startTime: string;