@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
@@ -15,6 +15,11 @@ import {
15
15
  } from "./booking-service.js";
16
16
  import { getBookingByUid, insertBooking } from "./bookings-repo.js";
17
17
  import { setSchedulingContext } from "./context.js";
18
+ import {
19
+ onBookingCreated,
20
+ onBookingNoShow,
21
+ onBookingRescheduled,
22
+ } from "./hooks.js";
18
23
  import {
19
24
  registerCalendarProvider,
20
25
  registerVideoProvider,
@@ -218,6 +223,34 @@ beforeEach(async () => {
218
223
  visibility TEXT NOT NULL DEFAULT 'private'
219
224
  );
220
225
  `);
226
+ await execute(`
227
+ CREATE TABLE workflow_shares (
228
+ id TEXT PRIMARY KEY,
229
+ resource_id TEXT NOT NULL,
230
+ principal_type TEXT NOT NULL,
231
+ principal_id TEXT NOT NULL,
232
+ role TEXT NOT NULL DEFAULT 'viewer',
233
+ created_by TEXT NOT NULL DEFAULT '',
234
+ created_at TEXT NOT NULL,
235
+ notified_at TEXT
236
+ );
237
+ `);
238
+ await execute(`
239
+ CREATE TABLE workflow_steps (
240
+ id TEXT PRIMARY KEY,
241
+ workflow_id TEXT NOT NULL,
242
+ "order" INTEGER NOT NULL DEFAULT 0,
243
+ action TEXT NOT NULL,
244
+ offset_minutes INTEGER NOT NULL DEFAULT 0,
245
+ send_to TEXT,
246
+ email_subject TEXT,
247
+ email_body TEXT,
248
+ sms_body TEXT,
249
+ webhook_url TEXT,
250
+ template TEXT,
251
+ created_at TEXT NOT NULL
252
+ );
253
+ `);
221
254
  await execute(`
222
255
  CREATE TABLE webhooks (
223
256
  id TEXT PRIMARY KEY,
@@ -235,6 +268,18 @@ beforeEach(async () => {
235
268
  visibility TEXT NOT NULL DEFAULT 'private'
236
269
  );
237
270
  `);
271
+ await execute(`
272
+ CREATE TABLE webhook_deliveries (
273
+ id TEXT PRIMARY KEY,
274
+ webhook_id TEXT NOT NULL,
275
+ triggered_at TEXT NOT NULL,
276
+ payload TEXT NOT NULL,
277
+ response_status INTEGER,
278
+ response_body TEXT,
279
+ success BOOLEAN NOT NULL DEFAULT false,
280
+ attempts INTEGER NOT NULL DEFAULT 0
281
+ );
282
+ `);
238
283
  await execute(`
239
284
  CREATE TABLE scheduled_reminders (
240
285
  id TEXT PRIMARY KEY,
@@ -354,7 +399,7 @@ async function seedEventType(eventType: EventType): Promise<void> {
354
399
  eventType.createdAt,
355
400
  eventType.updatedAt,
356
401
  eventType.ownerEmail ?? "local@localhost",
357
- null,
402
+ eventType.orgId ?? null,
358
403
  "private",
359
404
  ],
360
405
  });
@@ -419,7 +464,419 @@ describe("insertBooking", () => {
419
464
  });
420
465
  });
421
466
 
467
+ describe("booking webhook dispatch", () => {
468
+ it("runs private workflows scoped to the booking team", async () => {
469
+ const now = new Date().toISOString();
470
+ await execute({
471
+ sql: `INSERT INTO workflows (
472
+ id, name, trigger, team_id, disabled, active_on_event_type_ids,
473
+ created_at, updated_at
474
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
475
+ args: [
476
+ "workflow-team-private",
477
+ "Team booking workflow",
478
+ "new-booking",
479
+ "team-b",
480
+ false,
481
+ '["event-type-1"]',
482
+ now,
483
+ now,
484
+ ],
485
+ });
486
+ await execute({
487
+ sql: `INSERT INTO workflow_steps (
488
+ id, workflow_id, action, offset_minutes, created_at
489
+ ) VALUES (?, ?, ?, ?, ?)`,
490
+ args: [
491
+ "workflow-step-team",
492
+ "workflow-team-private",
493
+ "email-host",
494
+ 0,
495
+ now,
496
+ ],
497
+ });
498
+
499
+ await createBooking({
500
+ eventType: makeEventType({
501
+ schedulingType: "round-robin",
502
+ teamId: "team-b",
503
+ orgId: "org-b",
504
+ }),
505
+ hostEmail: "team-host@example.com",
506
+ startTime: "2026-08-03T11:00:00.000Z",
507
+ endTime: "2026-08-03T11:30:00.000Z",
508
+ timezone: "UTC",
509
+ attendee: { email: ATTENDEE_EMAIL, name: "Attendee One" },
510
+ });
511
+
512
+ const { rows } = await execute(
513
+ "SELECT workflow_step_id FROM scheduled_reminders",
514
+ );
515
+ expect(rows.map((row) => row.workflow_step_id)).toEqual([
516
+ "workflow-step-team",
517
+ ]);
518
+ });
519
+
520
+ it("excludes another member's private webhook but preserves org-visible webhooks", async () => {
521
+ const now = new Date().toISOString();
522
+ const eventTypes = JSON.stringify(["event-type-1"]);
523
+ await execute({
524
+ sql: `INSERT INTO workflows (
525
+ id, name, trigger, disabled, active_on_event_type_ids,
526
+ created_at, updated_at, owner_email, org_id
527
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
528
+ args: [
529
+ "workflow-user-a",
530
+ "Booking workflow",
531
+ "new-booking",
532
+ false,
533
+ eventTypes,
534
+ now,
535
+ now,
536
+ "user-a@example.com",
537
+ "org-a",
538
+ "workflow-user-b",
539
+ "Booking workflow",
540
+ "new-booking",
541
+ false,
542
+ eventTypes,
543
+ now,
544
+ now,
545
+ "user-b@example.com",
546
+ "org-b",
547
+ ],
548
+ });
549
+ await execute({
550
+ sql: `INSERT INTO workflow_steps (
551
+ id, workflow_id, action, offset_minutes, created_at
552
+ ) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`,
553
+ args: [
554
+ "workflow-step-user-a",
555
+ "workflow-user-a",
556
+ "email-host",
557
+ 0,
558
+ now,
559
+ "workflow-step-user-b",
560
+ "workflow-user-b",
561
+ "email-host",
562
+ 0,
563
+ now,
564
+ ],
565
+ });
566
+ await execute({
567
+ sql: `INSERT INTO webhooks (
568
+ id, subscriber_url, event_triggers, created_at, updated_at,
569
+ owner_email, org_id, visibility
570
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?),
571
+ (?, ?, ?, ?, ?, ?, ?, ?)`,
572
+ args: [
573
+ "webhook-user-a-private",
574
+ "https://example.test/webhook",
575
+ '["BOOKING_CREATED"]',
576
+ now,
577
+ now,
578
+ "user-a@example.com",
579
+ "org-b",
580
+ "private",
581
+ "webhook-user-a-org",
582
+ "https://example.test/webhook",
583
+ '["BOOKING_CREATED"]',
584
+ now,
585
+ now,
586
+ "user-a@example.com",
587
+ "org-b",
588
+ "org",
589
+ "webhook-user-b",
590
+ "https://example.test/webhook",
591
+ '["BOOKING_CREATED"]',
592
+ now,
593
+ now,
594
+ "user-b@example.com",
595
+ "org-b",
596
+ "private",
597
+ ],
598
+ });
599
+
600
+ const booking = await insertBooking({
601
+ eventTypeId: "event-type-1",
602
+ hostEmail: "user-b@example.com",
603
+ ownerEmail: "user-b@example.com",
604
+ orgId: "org-b",
605
+ title: "Private booking",
606
+ startTime: "2026-08-03T10:00:00.000Z",
607
+ endTime: "2026-08-03T10:30:00.000Z",
608
+ timezone: "UTC",
609
+ attendees: [{ email: ATTENDEE_EMAIL, name: "Attendee One" }],
610
+ });
611
+ expect(booking.cancelToken).toBeTruthy();
612
+ expect(booking.rescheduleToken).toBeTruthy();
613
+
614
+ await onBookingCreated(booking);
615
+
616
+ const { rows } = await execute(
617
+ "SELECT webhook_id FROM webhook_deliveries ORDER BY webhook_id",
618
+ );
619
+ expect(rows.map((row) => row.webhook_id)).toEqual([
620
+ "webhook-user-a-org",
621
+ "webhook-user-b",
622
+ ]);
623
+ const { rows: deliveries } = await execute(
624
+ "SELECT payload FROM webhook_deliveries",
625
+ );
626
+ for (const delivery of deliveries) {
627
+ const payload = JSON.parse(String(delivery.payload));
628
+ expect(payload.booking).not.toHaveProperty("cancelToken");
629
+ expect(payload.booking).not.toHaveProperty("rescheduleToken");
630
+ }
631
+ const { rows: reminders } = await execute(
632
+ "SELECT workflow_step_id FROM scheduled_reminders ORDER BY workflow_step_id",
633
+ );
634
+ expect(reminders.map((row) => row.workflow_step_id)).toEqual([
635
+ "workflow-step-user-b",
636
+ ]);
637
+ });
638
+
639
+ it("keeps team scope when a persisted booking is rescheduled", async () => {
640
+ const now = new Date().toISOString();
641
+ const eventType = makeEventType({
642
+ id: "team-event",
643
+ orgId: "org-b",
644
+ teamId: "team-b",
645
+ });
646
+ await seedEventType(eventType);
647
+ await execute({
648
+ sql: `INSERT INTO webhooks (
649
+ id, subscriber_url, event_triggers, created_at, updated_at,
650
+ owner_email, org_id, visibility, team_id
651
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
652
+ args: [
653
+ "team-webhook",
654
+ "https://example.test/webhook",
655
+ '["BOOKING_RESCHEDULED","BOOKING_NO_SHOW"]',
656
+ now,
657
+ now,
658
+ "team-owner@example.com",
659
+ "org-b",
660
+ "org",
661
+ "team-b",
662
+ ],
663
+ });
664
+ await execute({
665
+ sql: `INSERT INTO workflows (
666
+ id, name, trigger, disabled, active_on_event_type_ids,
667
+ created_at, updated_at, owner_email, org_id, team_id
668
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
669
+ args: [
670
+ "team-no-show-workflow",
671
+ "Team no-show workflow",
672
+ "no-show",
673
+ false,
674
+ '["team-event"]',
675
+ now,
676
+ now,
677
+ "team-owner@example.com",
678
+ "org-b",
679
+ "team-b",
680
+ ],
681
+ });
682
+ await execute({
683
+ sql: `INSERT INTO workflow_steps (
684
+ id, workflow_id, action, offset_minutes, created_at
685
+ ) VALUES (?, ?, ?, ?, ?)`,
686
+ args: [
687
+ "team-no-show-step",
688
+ "team-no-show-workflow",
689
+ "email-host",
690
+ 0,
691
+ now,
692
+ ],
693
+ });
694
+ const created = await createBooking({
695
+ eventType,
696
+ hostEmail: "user-b@example.com",
697
+ startTime: "2026-08-03T10:00:00.000Z",
698
+ endTime: "2026-08-03T10:30:00.000Z",
699
+ timezone: "UTC",
700
+ attendee: { email: ATTENDEE_EMAIL, name: "Attendee One" },
701
+ });
702
+ const persisted = await getBookingByUid(created.uid);
703
+
704
+ expect(created.teamId).toBe("team-b");
705
+ expect(persisted?.teamId).toBeUndefined();
706
+ await onBookingRescheduled(persisted!, created);
707
+ await onBookingNoShow(persisted!);
708
+
709
+ const { rows } = await execute("SELECT webhook_id FROM webhook_deliveries");
710
+ expect(rows.map((row) => row.webhook_id)).toEqual([
711
+ "team-webhook",
712
+ "team-webhook",
713
+ ]);
714
+ const { rows: reminders } = await execute(
715
+ "SELECT workflow_step_id FROM scheduled_reminders",
716
+ );
717
+ expect(reminders.map((row) => row.workflow_step_id)).toEqual([
718
+ "team-no-show-step",
719
+ ]);
720
+ });
721
+
722
+ it("keeps a booking in its event type org and excludes another member's private hooks", async () => {
723
+ const now = new Date().toISOString();
724
+ const eventTypes = JSON.stringify(["event-type-1"]);
725
+ await execute({
726
+ sql: `INSERT INTO workflows (
727
+ id, name, trigger, disabled, active_on_event_type_ids,
728
+ created_at, updated_at, owner_email, org_id, visibility
729
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?),
730
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
731
+ args: [
732
+ "workflow-user-a-private",
733
+ "Private workflow",
734
+ "new-booking",
735
+ false,
736
+ eventTypes,
737
+ now,
738
+ now,
739
+ "user-a@example.com",
740
+ "org-b",
741
+ "private",
742
+ "workflow-user-a-org",
743
+ "Org workflow",
744
+ "new-booking",
745
+ false,
746
+ eventTypes,
747
+ now,
748
+ now,
749
+ "user-a@example.com",
750
+ "org-b",
751
+ "org",
752
+ "workflow-user-b",
753
+ "User B workflow",
754
+ "new-booking",
755
+ false,
756
+ eventTypes,
757
+ now,
758
+ now,
759
+ "user-b@example.com",
760
+ "org-b",
761
+ "private",
762
+ ],
763
+ });
764
+ await execute({
765
+ sql: `INSERT INTO workflow_steps (
766
+ id, workflow_id, action, offset_minutes, created_at
767
+ ) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`,
768
+ args: [
769
+ "workflow-step-user-a-private",
770
+ "workflow-user-a-private",
771
+ "email-host",
772
+ 0,
773
+ now,
774
+ "workflow-step-user-a-org",
775
+ "workflow-user-a-org",
776
+ "email-host",
777
+ 0,
778
+ now,
779
+ "workflow-step-user-b",
780
+ "workflow-user-b",
781
+ "email-host",
782
+ 0,
783
+ now,
784
+ ],
785
+ });
786
+ await execute({
787
+ sql: `INSERT INTO webhooks (
788
+ id, subscriber_url, event_triggers, created_at, updated_at,
789
+ owner_email, org_id, visibility
790
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?),
791
+ (?, ?, ?, ?, ?, ?, ?, ?)`,
792
+ args: [
793
+ "webhook-user-a-private",
794
+ "https://example.test/webhook",
795
+ '["BOOKING_CREATED"]',
796
+ now,
797
+ now,
798
+ "user-a@example.com",
799
+ "org-b",
800
+ "private",
801
+ "webhook-user-a-other-org",
802
+ "https://example.test/webhook",
803
+ '["BOOKING_CREATED"]',
804
+ now,
805
+ now,
806
+ "user-a@example.com",
807
+ "org-a",
808
+ "org",
809
+ "webhook-user-b",
810
+ "https://example.test/webhook",
811
+ '["BOOKING_CREATED"]',
812
+ now,
813
+ now,
814
+ "user-b@example.com",
815
+ "org-b",
816
+ "private",
817
+ ],
818
+ });
819
+ await execute({
820
+ sql: `INSERT INTO webhooks (
821
+ id, subscriber_url, event_triggers, created_at, updated_at,
822
+ owner_email, org_id, visibility, team_id
823
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
824
+ args: [
825
+ "webhook-user-b-other-team",
826
+ "https://example.test/webhook",
827
+ '["BOOKING_CREATED"]',
828
+ now,
829
+ now,
830
+ "user-b@example.com",
831
+ "org-b",
832
+ "private",
833
+ "team-a",
834
+ ],
835
+ });
836
+
837
+ await createBooking({
838
+ eventType: makeEventType({ orgId: "org-b", teamId: "team-b" }),
839
+ orgId: "org-b",
840
+ hostEmail: "user-b@example.com",
841
+ startTime: "2026-08-03T10:00:00.000Z",
842
+ endTime: "2026-08-03T10:30:00.000Z",
843
+ timezone: "UTC",
844
+ attendee: { email: ATTENDEE_EMAIL, name: "Attendee One" },
845
+ });
846
+
847
+ const { rows: bookingRows } = await execute(
848
+ "SELECT org_id FROM bookings WHERE host_email = 'user-b@example.com'",
849
+ );
850
+ expect(bookingRows.map((row) => row.org_id)).toEqual(["org-b"]);
851
+ const { rows: deliveries } = await execute(
852
+ "SELECT webhook_id FROM webhook_deliveries ORDER BY webhook_id",
853
+ );
854
+ expect(deliveries.map((row) => row.webhook_id)).toEqual(["webhook-user-b"]);
855
+ const { rows: reminders } = await execute(
856
+ "SELECT workflow_step_id FROM scheduled_reminders ORDER BY workflow_step_id",
857
+ );
858
+ expect(reminders.map((row) => row.workflow_step_id)).toEqual([
859
+ "workflow-step-user-a-org",
860
+ "workflow-step-user-b",
861
+ ]);
862
+ });
863
+ });
864
+
422
865
  describe("createBooking", () => {
866
+ it("rejects a legacy org id absent from the event type", async () => {
867
+ await expect(
868
+ createBooking({
869
+ eventType: makeEventType(),
870
+ orgId: "org-b",
871
+ hostEmail: HOST_EMAIL,
872
+ startTime: "2026-08-03T10:00:00.000Z",
873
+ endTime: "2026-08-03T10:30:00.000Z",
874
+ timezone: "UTC",
875
+ attendee: { email: ATTENDEE_EMAIL, name: "Attendee One" },
876
+ }),
877
+ ).rejects.toThrow("Booking orgId must match the event type organization");
878
+ });
879
+
423
880
  it("creates a booking with attendees and references on a free slot", async () => {
424
881
  const now = new Date().toISOString();
425
882
  await execute({
@@ -59,6 +59,9 @@ export async function createBooking(
59
59
  input: CreateBookingInput,
60
60
  ): Promise<Booking> {
61
61
  const eventType = input.eventType;
62
+ if (input.orgId !== undefined && input.orgId !== eventType.orgId) {
63
+ throw new Error("Booking orgId must match the event type organization");
64
+ }
62
65
  const title =
63
66
  input.title ??
64
67
  eventType.eventName?.replace("{attendeeName}", input.attendee.name) ??
@@ -72,24 +75,27 @@ export async function createBooking(
72
75
  afterEventBuffer: eventType.afterEventBuffer,
73
76
  excludeBookingUid: input.fromReschedule,
74
77
  });
75
- const booking = await insertBooking({
76
- eventTypeId: eventType.id,
77
- hostEmail: input.hostEmail,
78
- title,
79
- description: input.description,
80
- startTime: input.startTime,
81
- endTime: input.endTime,
82
- timezone: input.timezone,
83
- status: eventType.requiresConfirmation ? "pending" : "confirmed",
84
- location: input.location ?? eventType.locations[0],
85
- attendees,
86
- customResponses: input.customResponses,
87
- iCalUid: input.iCalUid,
88
- iCalSequence: input.iCalSequence,
89
- fromReschedule: input.fromReschedule,
90
- ownerEmail: input.hostEmail,
91
- orgId: input.orgId,
92
- });
78
+ const booking = {
79
+ ...(await insertBooking({
80
+ eventTypeId: eventType.id,
81
+ hostEmail: input.hostEmail,
82
+ title,
83
+ description: input.description,
84
+ startTime: input.startTime,
85
+ endTime: input.endTime,
86
+ timezone: input.timezone,
87
+ status: eventType.requiresConfirmation ? "pending" : "confirmed",
88
+ location: input.location ?? eventType.locations[0],
89
+ attendees,
90
+ customResponses: input.customResponses,
91
+ iCalUid: input.iCalUid,
92
+ iCalSequence: input.iCalSequence,
93
+ fromReschedule: input.fromReschedule,
94
+ ownerEmail: input.hostEmail,
95
+ orgId: eventType.orgId,
96
+ })),
97
+ teamId: eventType.teamId,
98
+ };
93
99
 
94
100
  let usableZoomMeeting = false;
95
101
 
@@ -141,7 +147,7 @@ export async function createBooking(
141
147
 
142
148
  const final = await getBookingByUid(booking.uid);
143
149
  if (!final) throw new Error("Booking disappeared after creation");
144
- return final;
150
+ return { ...final, teamId: eventType.teamId };
145
151
  }
146
152
 
147
153
  export async function rescheduleBooking(input: {
@@ -247,7 +253,8 @@ export async function cancelBooking(input: {
247
253
  }
248
254
  }
249
255
 
250
- await onBookingCancelled(booking);
256
+ const eventType = await getEventTypeById(booking.eventTypeId);
257
+ await onBookingCancelled({ ...booking, teamId: eventType?.teamId });
251
258
  return (await getBookingByUid(input.uid))!;
252
259
  }
253
260
 
@@ -1,5 +1,15 @@
1
1
  import { accessFilter } from "@agent-native/core/sharing";
2
- import { eq, and, gte, lt, or, desc, asc, isNotNull } from "drizzle-orm";
2
+ import {
3
+ eq,
4
+ and,
5
+ gte,
6
+ lt,
7
+ or,
8
+ desc,
9
+ asc,
10
+ isNotNull,
11
+ isNull,
12
+ } from "drizzle-orm";
3
13
  import { nanoid } from "nanoid";
4
14
 
5
15
  import type {
@@ -21,6 +31,8 @@ function rowToBooking(
21
31
  uid: row.uid,
22
32
  eventTypeId: row.eventTypeId,
23
33
  hostEmail: row.hostEmail,
34
+ ownerEmail: row.ownerEmail ?? row.hostEmail,
35
+ orgId: row.orgId ?? null,
24
36
  title: row.title,
25
37
  description: row.description ?? undefined,
26
38
  startTime: row.startTime,
@@ -74,6 +86,7 @@ function parseJson<T = any>(s: string | null | undefined): T | undefined {
74
86
  export async function getBookingByUid(uid: string): Promise<Booking | null> {
75
87
  const { getDb, schema } = getSchedulingContext();
76
88
  const db = getDb();
89
+ // guard:allow-unscoped — opaque booking UID is an authorization lookup; all public callers verify host or bearer token before exposing or mutating the row
77
90
  const rows = await db
78
91
  .select()
79
92
  .from(schema.bookings)
@@ -167,9 +180,11 @@ export async function listBookings(
167
180
  }
168
181
 
169
182
  export async function countBookingsByHostInRange(
183
+ eventTypeId: string,
170
184
  hostEmail: string,
171
185
  fromIso: string,
172
186
  toIso: string,
187
+ orgId?: string | null,
173
188
  ): Promise<number> {
174
189
  const { getDb, schema } = getSchedulingContext();
175
190
  const rows = await getDb()
@@ -178,6 +193,11 @@ export async function countBookingsByHostInRange(
178
193
  .where(
179
194
  and(
180
195
  eq(schema.bookings.hostEmail, hostEmail),
196
+ eq(schema.bookings.ownerEmail, hostEmail),
197
+ orgId
198
+ ? eq(schema.bookings.orgId, orgId)
199
+ : isNull(schema.bookings.orgId),
200
+ eq(schema.bookings.eventTypeId, eventTypeId),
181
201
  gte(schema.bookings.startTime, fromIso),
182
202
  lt(schema.bookings.startTime, toIso),
183
203
  eq(schema.bookings.status, "confirmed"),
@@ -31,6 +31,7 @@ function rowToEventType(row: any): EventType {
31
31
  position: row.position,
32
32
  schedulingType: row.schedulingType,
33
33
  ownerEmail: row.ownerEmail ?? undefined,
34
+ orgId: row.orgId ?? undefined,
34
35
  teamId: row.teamId ?? undefined,
35
36
  scheduleId: row.scheduleId ?? undefined,
36
37
  locations: parseJson<Location[]>(row.locations) ?? [],
@@ -297,10 +298,23 @@ export async function updateEventType(
297
298
 
298
299
  dbPatch.updatedAt = new Date().toISOString();
299
300
 
300
- await db
301
+ const updatedRows = await db
301
302
  .update(schema.eventTypes)
302
303
  .set(dbPatch)
303
- .where(eq(schema.eventTypes.id, id));
304
+ .where(
305
+ and(
306
+ eq(schema.eventTypes.id, id),
307
+ accessFilter(
308
+ schema.eventTypes,
309
+ schema.eventTypeShares,
310
+ undefined,
311
+ "editor",
312
+ ),
313
+ ),
314
+ )
315
+ .returning({ id: schema.eventTypes.id });
316
+ if (updatedRows.length === 0)
317
+ throw new Error("Event type not found or not editable");
304
318
  const updated = await getEventTypeById(id);
305
319
  if (!updated) throw new Error("Failed to update event type");
306
320
  return updated;