@agent-native/scheduling 0.1.24 → 0.1.25
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.
- package/dist/actions/_helpers.d.ts +10 -0
- package/dist/actions/_helpers.d.ts.map +1 -1
- package/dist/actions/_helpers.js +14 -0
- package/dist/actions/_helpers.js.map +1 -1
- package/dist/actions/add-booking-attendee.js +2 -0
- package/dist/actions/add-booking-attendee.js.map +1 -1
- package/dist/actions/add-booking-note.js +2 -1
- package/dist/actions/add-booking-note.js.map +1 -1
- package/dist/actions/confirm-booking.js +5 -0
- package/dist/actions/confirm-booking.js.map +1 -1
- package/dist/actions/duplicate-event-type.js +2 -0
- package/dist/actions/duplicate-event-type.js.map +1 -1
- package/dist/actions/get-booking.d.ts +3 -1
- package/dist/actions/get-booking.js +8 -1
- package/dist/actions/get-booking.js.map +1 -1
- package/dist/actions/mark-no-show.js +6 -0
- package/dist/actions/mark-no-show.js.map +1 -1
- package/dist/actions/remove-booking-attendee.js +2 -0
- package/dist/actions/remove-booking-attendee.js.map +1 -1
- package/dist/actions/revoke-private-link.js +18 -1
- package/dist/actions/revoke-private-link.js.map +1 -1
- package/dist/actions/send-reschedule-link.js +2 -0
- package/dist/actions/send-reschedule-link.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/_helpers.ts +18 -0
- package/src/actions/add-booking-attendee.ts +2 -0
- package/src/actions/add-booking-note.ts +2 -1
- package/src/actions/booking-authz.spec.ts +288 -0
- package/src/actions/confirm-booking.ts +4 -0
- package/src/actions/duplicate-event-type.ts +2 -0
- package/src/actions/event-type-authz.spec.ts +209 -0
- package/src/actions/get-booking.ts +7 -1
- package/src/actions/mark-no-show.ts +5 -0
- package/src/actions/remove-booking-attendee.ts +2 -0
- package/src/actions/revoke-private-link.ts +16 -1
- package/src/actions/send-reschedule-link.ts +2 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export declare function currentUserEmail(): string;
|
|
2
2
|
export declare function currentUserEmailOrNull(): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* Verify the current user is the host of a booking. Throws "Not authorized to
|
|
5
|
+
* <action>" if not. Mirrors the `isHost` half of the `isHost || hasToken`
|
|
6
|
+
* guard used by cancel-booking/reschedule-booking, for booking actions that
|
|
7
|
+
* don't accept a public capability token (attendee/note/status mutations —
|
|
8
|
+
* host identity is the only legitimate caller for these).
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertBookingHost(booking: {
|
|
11
|
+
hostEmail: string;
|
|
12
|
+
}, action: string): void;
|
|
3
13
|
export declare function currentOrgId(): string | undefined;
|
|
4
14
|
export declare function assertTeamAdmin(teamId: string): Promise<void>;
|
|
5
15
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_helpers.d.ts","sourceRoot":"","sources":["../../src/actions/_helpers.ts"],"names":[],"mappings":"AAOA,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC;AAED,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjD;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBnE;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpE"}
|
|
1
|
+
{"version":3,"file":"_helpers.d.ts","sourceRoot":"","sources":["../../src/actions/_helpers.ts"],"names":[],"mappings":"AAOA,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC;AAED,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAEtD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC9B,MAAM,EAAE,MAAM,GACb,IAAI,CAMN;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjD;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBnE;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpE"}
|
package/dist/actions/_helpers.js
CHANGED
|
@@ -12,6 +12,20 @@ export function currentUserEmail() {
|
|
|
12
12
|
export function currentUserEmailOrNull() {
|
|
13
13
|
return getSchedulingContext().getCurrentUserEmail() ?? null;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Verify the current user is the host of a booking. Throws "Not authorized to
|
|
17
|
+
* <action>" if not. Mirrors the `isHost` half of the `isHost || hasToken`
|
|
18
|
+
* guard used by cancel-booking/reschedule-booking, for booking actions that
|
|
19
|
+
* don't accept a public capability token (attendee/note/status mutations —
|
|
20
|
+
* host identity is the only legitimate caller for these).
|
|
21
|
+
*/
|
|
22
|
+
export function assertBookingHost(booking, action) {
|
|
23
|
+
const userEmail = currentUserEmailOrNull();
|
|
24
|
+
const isHost = !!userEmail && userEmail === booking.hostEmail;
|
|
25
|
+
if (!isHost) {
|
|
26
|
+
throw new Error(`Not authorized to ${action}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
export function currentOrgId() {
|
|
16
30
|
return getSchedulingContext().getCurrentOrgId?.();
|
|
17
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_helpers.js","sourceRoot":"","sources":["../../src/actions/_helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,UAAU,gBAAgB;IAC9B,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC,mBAAmB,EAAE,CAAC;IAC3D,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACjD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,oBAAoB,EAAE,CAAC,mBAAmB,EAAE,IAAI,IAAI,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,oBAAoB,EAAE,CAAC,eAAe,EAAE,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc;IAClD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE;SACvB,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;SACzC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;SACxB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CACxC,CACF;SACA,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;IAC3B,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAc;IACnD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE;SACvB,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;SACzC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;SACxB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CACxC,CACF;SACA,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"_helpers.js","sourceRoot":"","sources":["../../src/actions/_helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,UAAU,gBAAgB;IAC9B,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC,mBAAmB,EAAE,CAAC;IAC3D,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACjD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,oBAAoB,EAAE,CAAC,mBAAmB,EAAE,IAAI,IAAI,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAA8B,EAC9B,MAAc;IAEd,MAAM,SAAS,GAAG,sBAAsB,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,IAAI,SAAS,KAAK,OAAO,CAAC,SAAS,CAAC;IAC9D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,EAAE,CAAC,CAAC;IACjD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,oBAAoB,EAAE,CAAC,eAAe,EAAE,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc;IAClD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE;SACvB,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;SACzC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;SACxB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CACxC,CACF;SACA,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;IAC3B,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAc;IACnD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE;SACvB,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;SACzC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;SACxB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CACxC,CACF;SACA,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;AACH,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { nanoid } from "nanoid";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
5
5
|
import { getSchedulingContext } from "../server/context.js";
|
|
6
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
6
7
|
export default defineAction({
|
|
7
8
|
description: "Add an attendee (or seat reservation) to a booking",
|
|
8
9
|
schema: z.object({
|
|
@@ -15,6 +16,7 @@ export default defineAction({
|
|
|
15
16
|
const booking = await getBookingByUid(args.uid);
|
|
16
17
|
if (!booking)
|
|
17
18
|
throw new Error(`Booking ${args.uid} not found`);
|
|
19
|
+
assertBookingHost(booking, "add an attendee to this booking");
|
|
18
20
|
const { getDb, schema } = getSchedulingContext();
|
|
19
21
|
await getDb()
|
|
20
22
|
.insert(schema.bookingAttendees)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-booking-attendee.js","sourceRoot":"","sources":["../../src/actions/add-booking-attendee.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"add-booking-attendee.js","sourceRoot":"","sources":["../../src/actions/add-booking-attendee.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,oDAAoD;IACjE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;QACf,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAChC,CAAC;IACF,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;QAC/D,iBAAiB,CAAC,OAAO,EAAE,iCAAiC,CAAC,CAAC;QAC9D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;QACjD,MAAM,KAAK,EAAE;aACV,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC;aAC/B,MAAM,CAAC;YACN,EAAE,EAAE,MAAM,EAAE;YACZ,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;YAC/B,MAAM,EAAE,KAAK;YACb,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;QACL,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACtD,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -3,7 +3,7 @@ import { nanoid } from "nanoid";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
5
5
|
import { getSchedulingContext } from "../server/context.js";
|
|
6
|
-
import { currentUserEmail } from "./_helpers.js";
|
|
6
|
+
import { assertBookingHost, currentUserEmail } from "./_helpers.js";
|
|
7
7
|
export default defineAction({
|
|
8
8
|
description: "Add an internal (host-only) note to a booking",
|
|
9
9
|
schema: z.object({ uid: z.string(), content: z.string() }),
|
|
@@ -11,6 +11,7 @@ export default defineAction({
|
|
|
11
11
|
const booking = await getBookingByUid(args.uid);
|
|
12
12
|
if (!booking)
|
|
13
13
|
throw new Error(`Booking ${args.uid} not found`);
|
|
14
|
+
assertBookingHost(booking, "add a note to this booking");
|
|
14
15
|
const { getDb, schema } = getSchedulingContext();
|
|
15
16
|
const id = nanoid();
|
|
16
17
|
await getDb().insert(schema.bookingNotes).values({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-booking-note.js","sourceRoot":"","sources":["../../src/actions/add-booking-note.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"add-booking-note.js","sourceRoot":"","sources":["../../src/actions/add-booking-note.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEpE,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,+CAA+C;IAC5D,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IAC1D,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;QAC/D,iBAAiB,CAAC,OAAO,EAAE,4BAA4B,CAAC,CAAC;QACzD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;QACjD,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;QACpB,MAAM,KAAK,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC;YAC/C,EAAE;YACF,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,WAAW,EAAE,gBAAgB,EAAE;YAC/B,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,CAAC;IAChB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { updateBookingStatus, getBookingByUid, } from "../server/bookings-repo.js";
|
|
4
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
4
5
|
export default defineAction({
|
|
5
6
|
description: "Confirm a pending booking (requires-confirmation flow)",
|
|
6
7
|
schema: z.object({ uid: z.string() }),
|
|
7
8
|
run: async (args) => {
|
|
9
|
+
const booking = await getBookingByUid(args.uid);
|
|
10
|
+
if (!booking)
|
|
11
|
+
throw new Error(`Booking ${args.uid} not found`);
|
|
12
|
+
assertBookingHost(booking, "confirm this booking");
|
|
8
13
|
await updateBookingStatus(args.uid, "confirmed");
|
|
9
14
|
return { booking: await getBookingByUid(args.uid) };
|
|
10
15
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirm-booking.js","sourceRoot":"","sources":["../../src/actions/confirm-booking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"confirm-booking.js","sourceRoot":"","sources":["../../src/actions/confirm-booking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,wDAAwD;IACrE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACrC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;QAC/D,iBAAiB,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC;QACnD,MAAM,mBAAmB,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACjD,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACtD,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getEventTypeById, createEventType, } from "../server/event-types-repo.js";
|
|
4
5
|
import { currentUserEmail, currentOrgId } from "./_helpers.js";
|
|
@@ -13,6 +14,7 @@ export default defineAction({
|
|
|
13
14
|
const source = await getEventTypeById(args.id);
|
|
14
15
|
if (!source)
|
|
15
16
|
throw new Error("Event type not found");
|
|
17
|
+
await assertAccess("event-type", args.id, "editor");
|
|
16
18
|
return {
|
|
17
19
|
eventType: await createEventType({
|
|
18
20
|
ownerEmail: source.teamId ? undefined : currentUserEmail(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"duplicate-event-type.js","sourceRoot":"","sources":["../../src/actions/duplicate-event-type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,gBAAgB,EAChB,eAAe,GAChB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE/D,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,yCAAyC;IACtD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;QACd,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAChC,CAAC;IACF,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACrD,OAAO;YACL,SAAS,EAAE,MAAM,eAAe,CAAC;gBAC/B,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,EAAE;gBAC1D,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK,EAAE,YAAY,EAAE;gBACrB,KAAK,EAAE,IAAI,CAAC,QAAQ,IAAI,GAAG,MAAM,CAAC,KAAK,SAAS;gBAChD,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;gBAChC,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,cAAc,EAAE,MAAM,CAAC,cAAc;gBACrC,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,UAAU,EAAE,MAAM,CAAC,UAAU;gBAC7B,KAAK,EAAE,MAAM,CAAC,KAAK;aACpB,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"duplicate-event-type.js","sourceRoot":"","sources":["../../src/actions/duplicate-event-type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,gBAAgB,EAChB,eAAe,GAChB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE/D,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,yCAAyC;IACtD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;QACd,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAChC,CAAC;IACF,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACrD,MAAM,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QACpD,OAAO;YACL,SAAS,EAAE,MAAM,eAAe,CAAC;gBAC/B,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,EAAE;gBAC1D,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK,EAAE,YAAY,EAAE;gBACrB,KAAK,EAAE,IAAI,CAAC,QAAQ,IAAI,GAAG,MAAM,CAAC,KAAK,SAAS;gBAChD,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;gBAChC,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,cAAc,EAAE,MAAM,CAAC,cAAc;gBACrC,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,UAAU,EAAE,MAAM,CAAC,UAAU;gBAC7B,KAAK,EAAE,MAAM,CAAC,KAAK;aACpB,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
declare const _default: import("@agent-native/core").ActionDefinition<{
|
|
2
2
|
uid: string;
|
|
3
3
|
}, {
|
|
4
|
-
booking:
|
|
4
|
+
booking: null;
|
|
5
|
+
} | {
|
|
6
|
+
booking: import("../index.js").Booking;
|
|
5
7
|
}>;
|
|
6
8
|
export default _default;
|
|
7
9
|
//# sourceMappingURL=get-booking.d.ts.map
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
4
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
4
5
|
export default defineAction({
|
|
5
6
|
description: "Get a booking by uid",
|
|
6
7
|
schema: z.object({ uid: z.string() }),
|
|
7
|
-
run: async (args) =>
|
|
8
|
+
run: async (args) => {
|
|
9
|
+
const booking = await getBookingByUid(args.uid);
|
|
10
|
+
if (!booking)
|
|
11
|
+
return { booking: null };
|
|
12
|
+
assertBookingHost(booking, "view this booking");
|
|
13
|
+
return { booking };
|
|
14
|
+
},
|
|
8
15
|
});
|
|
9
16
|
//# sourceMappingURL=get-booking.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-booking.js","sourceRoot":"","sources":["../../src/actions/get-booking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"get-booking.js","sourceRoot":"","sources":["../../src/actions/get-booking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,sBAAsB;IACnC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACrC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACvC,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;QAChD,OAAO,EAAE,OAAO,EAAE,CAAC;IACrB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { markNoShow } from "../server/booking-service.js";
|
|
4
|
+
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
5
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
4
6
|
export default defineAction({
|
|
5
7
|
description: "Mark an attendee as no-show on a booking",
|
|
6
8
|
schema: z.object({
|
|
@@ -8,6 +10,10 @@ export default defineAction({
|
|
|
8
10
|
attendeeEmail: z.string(),
|
|
9
11
|
}),
|
|
10
12
|
run: async (args) => {
|
|
13
|
+
const booking = await getBookingByUid(args.uid);
|
|
14
|
+
if (!booking)
|
|
15
|
+
throw new Error(`Booking ${args.uid} not found`);
|
|
16
|
+
assertBookingHost(booking, "mark no-show on this booking");
|
|
11
17
|
await markNoShow(args.uid, args.attendeeEmail);
|
|
12
18
|
return { ok: true };
|
|
13
19
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mark-no-show.js","sourceRoot":"","sources":["../../src/actions/mark-no-show.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"mark-no-show.js","sourceRoot":"","sources":["../../src/actions/mark-no-show.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,0CAA0C;IACvD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;QACf,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE;KAC1B,CAAC;IACF,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;QAC/D,iBAAiB,CAAC,OAAO,EAAE,8BAA8B,CAAC,CAAC;QAC3D,MAAM,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAC/C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { and, eq } from "drizzle-orm";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
5
5
|
import { getSchedulingContext } from "../server/context.js";
|
|
6
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
6
7
|
export default defineAction({
|
|
7
8
|
description: "Remove an attendee from a booking",
|
|
8
9
|
schema: z.object({ uid: z.string(), email: z.string() }),
|
|
@@ -10,6 +11,7 @@ export default defineAction({
|
|
|
10
11
|
const booking = await getBookingByUid(args.uid);
|
|
11
12
|
if (!booking)
|
|
12
13
|
throw new Error(`Booking ${args.uid} not found`);
|
|
14
|
+
assertBookingHost(booking, "remove an attendee from this booking");
|
|
13
15
|
const { getDb, schema } = getSchedulingContext();
|
|
14
16
|
await getDb()
|
|
15
17
|
.delete(schema.bookingAttendees)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remove-booking-attendee.js","sourceRoot":"","sources":["../../src/actions/remove-booking-attendee.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"remove-booking-attendee.js","sourceRoot":"","sources":["../../src/actions/remove-booking-attendee.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,mCAAmC;IAChD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACxD,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;QAC/D,iBAAiB,CAAC,OAAO,EAAE,sCAAsC,CAAC,CAAC;QACnE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;QACjD,MAAM,KAAK,EAAE;aACV,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC;aAC/B,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC,EACjD,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAC9C,CACF,CAAC;QACJ,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACtD,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
2
3
|
import { eq } from "drizzle-orm";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { getSchedulingContext } from "../server/context.js";
|
|
@@ -7,7 +8,23 @@ export default defineAction({
|
|
|
7
8
|
schema: z.object({ hash: z.string() }),
|
|
8
9
|
run: async (args) => {
|
|
9
10
|
const { getDb, schema } = getSchedulingContext();
|
|
10
|
-
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const [link] = await db
|
|
13
|
+
.select({ eventTypeId: schema.hashedLinks.eventTypeId })
|
|
14
|
+
.from(schema.hashedLinks)
|
|
15
|
+
.where(eq(schema.hashedLinks.hash, args.hash))
|
|
16
|
+
.limit(1);
|
|
17
|
+
if (!link)
|
|
18
|
+
return { ok: true };
|
|
19
|
+
try {
|
|
20
|
+
await assertAccess("event-type", link.eventTypeId, "editor");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Keep private-link hashes unprobeable: callers without access receive
|
|
24
|
+
// the same idempotent result as callers presenting an unknown hash.
|
|
25
|
+
return { ok: true };
|
|
26
|
+
}
|
|
27
|
+
await db
|
|
11
28
|
.delete(schema.hashedLinks)
|
|
12
29
|
.where(eq(schema.hashedLinks.hash, args.hash));
|
|
13
30
|
return { ok: true };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revoke-private-link.js","sourceRoot":"","sources":["../../src/actions/revoke-private-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,8BAA8B;IAC3C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACtC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;QACjD,MAAM,KAAK,EAAE;
|
|
1
|
+
{"version":3,"file":"revoke-private-link.js","sourceRoot":"","sources":["../../src/actions/revoke-private-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,eAAe,YAAY,CAAC;IAC1B,WAAW,EAAE,8BAA8B;IAC3C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACtC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;QACjD,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;QACnB,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,EAAE;aACpB,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC;aACvD,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;aACxB,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;aAC7C,KAAK,CAAC,CAAC,CAAC,CAAC;QACZ,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;YACvE,oEAAoE;YACpE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;QACD,MAAM,EAAE;aACL,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;aAC1B,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACjD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { defineAction } from "@agent-native/core";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
4
4
|
import { getSchedulingContext } from "../server/context.js";
|
|
5
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
5
6
|
export default defineAction({
|
|
6
7
|
description: "Return the public reschedule URL for a booking (send it to an attendee)",
|
|
7
8
|
schema: z.object({ uid: z.string() }),
|
|
@@ -9,6 +10,7 @@ export default defineAction({
|
|
|
9
10
|
const booking = await getBookingByUid(args.uid);
|
|
10
11
|
if (!booking)
|
|
11
12
|
throw new Error(`Booking ${args.uid} not found`);
|
|
13
|
+
assertBookingHost(booking, "view the reschedule link for this booking");
|
|
12
14
|
const baseUrl = getSchedulingContext().publicBaseUrl ?? "";
|
|
13
15
|
return {
|
|
14
16
|
url: `${baseUrl}/reschedule/${booking.uid}?token=${booking.rescheduleToken}`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-reschedule-link.js","sourceRoot":"","sources":["../../src/actions/send-reschedule-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"send-reschedule-link.js","sourceRoot":"","sources":["../../src/actions/send-reschedule-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,eAAe,YAAY,CAAC;IAC1B,WAAW,EACT,yEAAyE;IAC3E,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACrC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;QAC/D,iBAAiB,CAAC,OAAO,EAAE,2CAA2C,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,oBAAoB,EAAE,CAAC,aAAa,IAAI,EAAE,CAAC;QAC3D,OAAO;YACL,GAAG,EAAE,GAAG,OAAO,eAAe,OAAO,CAAC,GAAG,UAAU,OAAO,CAAC,eAAe,EAAE;SAC7E,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/scheduling",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"description": "Scheduling primitives for agent-native apps — event types, availability, bookings, team scheduling, workflows, and routing forms.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"nanoid": "^5.1.9",
|
|
54
54
|
"rrule": "^2.8.1",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
|
-
"@agent-native/toolkit": "^0.4.
|
|
56
|
+
"@agent-native/toolkit": "^0.4.7"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@libsql/client": "^0.15.8",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"react": "^19.2.5",
|
|
65
65
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
66
66
|
"vitest": "^4.1.5",
|
|
67
|
-
"@agent-native/core": "0.
|
|
67
|
+
"@agent-native/core": "0.98.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@agent-native/core": ">=0.8.0",
|
package/src/actions/_helpers.ts
CHANGED
|
@@ -15,6 +15,24 @@ export function currentUserEmailOrNull(): string | null {
|
|
|
15
15
|
return getSchedulingContext().getCurrentUserEmail() ?? null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Verify the current user is the host of a booking. Throws "Not authorized to
|
|
20
|
+
* <action>" if not. Mirrors the `isHost` half of the `isHost || hasToken`
|
|
21
|
+
* guard used by cancel-booking/reschedule-booking, for booking actions that
|
|
22
|
+
* don't accept a public capability token (attendee/note/status mutations —
|
|
23
|
+
* host identity is the only legitimate caller for these).
|
|
24
|
+
*/
|
|
25
|
+
export function assertBookingHost(
|
|
26
|
+
booking: { hostEmail: string },
|
|
27
|
+
action: string,
|
|
28
|
+
): void {
|
|
29
|
+
const userEmail = currentUserEmailOrNull();
|
|
30
|
+
const isHost = !!userEmail && userEmail === booking.hostEmail;
|
|
31
|
+
if (!isHost) {
|
|
32
|
+
throw new Error(`Not authorized to ${action}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
export function currentOrgId(): string | undefined {
|
|
19
37
|
return getSchedulingContext().getCurrentOrgId?.();
|
|
20
38
|
}
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
6
6
|
import { getSchedulingContext } from "../server/context.js";
|
|
7
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
7
8
|
|
|
8
9
|
export default defineAction({
|
|
9
10
|
description: "Add an attendee (or seat reservation) to a booking",
|
|
@@ -16,6 +17,7 @@ export default defineAction({
|
|
|
16
17
|
run: async (args) => {
|
|
17
18
|
const booking = await getBookingByUid(args.uid);
|
|
18
19
|
if (!booking) throw new Error(`Booking ${args.uid} not found`);
|
|
20
|
+
assertBookingHost(booking, "add an attendee to this booking");
|
|
19
21
|
const { getDb, schema } = getSchedulingContext();
|
|
20
22
|
await getDb()
|
|
21
23
|
.insert(schema.bookingAttendees)
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
6
6
|
import { getSchedulingContext } from "../server/context.js";
|
|
7
|
-
import { currentUserEmail } from "./_helpers.js";
|
|
7
|
+
import { assertBookingHost, currentUserEmail } from "./_helpers.js";
|
|
8
8
|
|
|
9
9
|
export default defineAction({
|
|
10
10
|
description: "Add an internal (host-only) note to a booking",
|
|
@@ -12,6 +12,7 @@ export default defineAction({
|
|
|
12
12
|
run: async (args) => {
|
|
13
13
|
const booking = await getBookingByUid(args.uid);
|
|
14
14
|
if (!booking) throw new Error(`Booking ${args.uid} not found`);
|
|
15
|
+
assertBookingHost(booking, "add a note to this booking");
|
|
15
16
|
const { getDb, schema } = getSchedulingContext();
|
|
16
17
|
const id = nanoid();
|
|
17
18
|
await getDb().insert(schema.bookingNotes).values({
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tenant write (IDOR) regression tests for booking actions that
|
|
3
|
+
* mutate — or disclose a capability token for — a booking that belongs to
|
|
4
|
+
* another host. Mirrors the `isHost` guard already proven in
|
|
5
|
+
* `cancel-booking.ts` / `reschedule-booking.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { createClient, type Client } from "@libsql/client";
|
|
13
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import * as schema from "../schema/index.js";
|
|
17
|
+
import { setSchedulingContext } from "../server/context.js";
|
|
18
|
+
import addBookingAttendee from "./add-booking-attendee.js";
|
|
19
|
+
import addBookingNote from "./add-booking-note.js";
|
|
20
|
+
import confirmBooking from "./confirm-booking.js";
|
|
21
|
+
import markNoShow from "./mark-no-show.js";
|
|
22
|
+
import removeBookingAttendee from "./remove-booking-attendee.js";
|
|
23
|
+
import sendRescheduleLink from "./send-reschedule-link.js";
|
|
24
|
+
|
|
25
|
+
const HOST_EMAIL = "host@example.com";
|
|
26
|
+
const OUTSIDER_EMAIL = "outsider@example.com";
|
|
27
|
+
const BOOKING_UID = "booking-uid-1";
|
|
28
|
+
const ATTENDEE_EMAIL = "attendee@example.com";
|
|
29
|
+
|
|
30
|
+
let client: Client;
|
|
31
|
+
let dbDir: string;
|
|
32
|
+
let currentUserEmail: string | undefined;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
dbDir = mkdtempSync(join(tmpdir(), "scheduling-booking-authz-test-"));
|
|
36
|
+
client = createClient({ url: `file:${join(dbDir, `${randomUUID()}.db`)}` });
|
|
37
|
+
await client.execute(`
|
|
38
|
+
CREATE TABLE bookings (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
uid TEXT NOT NULL UNIQUE,
|
|
41
|
+
event_type_id TEXT NOT NULL,
|
|
42
|
+
host_email TEXT NOT NULL,
|
|
43
|
+
title TEXT NOT NULL,
|
|
44
|
+
description TEXT,
|
|
45
|
+
start_time TEXT NOT NULL,
|
|
46
|
+
end_time TEXT NOT NULL,
|
|
47
|
+
timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
48
|
+
status TEXT NOT NULL DEFAULT 'confirmed',
|
|
49
|
+
location TEXT,
|
|
50
|
+
custom_responses TEXT,
|
|
51
|
+
cancel_token TEXT,
|
|
52
|
+
reschedule_token TEXT,
|
|
53
|
+
from_reschedule TEXT,
|
|
54
|
+
cancellation_reason TEXT,
|
|
55
|
+
rescheduling_reason TEXT,
|
|
56
|
+
ical_uid TEXT NOT NULL,
|
|
57
|
+
ical_sequence INTEGER NOT NULL DEFAULT 0,
|
|
58
|
+
recurring_event_id TEXT,
|
|
59
|
+
paid INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
no_show_host INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
metadata TEXT,
|
|
62
|
+
created_at TEXT NOT NULL,
|
|
63
|
+
updated_at TEXT NOT NULL,
|
|
64
|
+
owner_email TEXT NOT NULL DEFAULT 'local@localhost',
|
|
65
|
+
org_id TEXT,
|
|
66
|
+
visibility TEXT NOT NULL DEFAULT 'private'
|
|
67
|
+
);
|
|
68
|
+
`);
|
|
69
|
+
await client.execute(`
|
|
70
|
+
CREATE TABLE booking_attendees (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
booking_id TEXT NOT NULL,
|
|
73
|
+
email TEXT NOT NULL,
|
|
74
|
+
name TEXT NOT NULL,
|
|
75
|
+
timezone TEXT,
|
|
76
|
+
locale TEXT,
|
|
77
|
+
no_show INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
created_at TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
`);
|
|
81
|
+
await client.execute(`
|
|
82
|
+
CREATE TABLE booking_references (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
booking_id TEXT NOT NULL,
|
|
85
|
+
type TEXT NOT NULL,
|
|
86
|
+
external_id TEXT NOT NULL,
|
|
87
|
+
meeting_url TEXT,
|
|
88
|
+
meeting_password TEXT,
|
|
89
|
+
credential_id TEXT,
|
|
90
|
+
created_at TEXT NOT NULL
|
|
91
|
+
);
|
|
92
|
+
`);
|
|
93
|
+
await client.execute(`
|
|
94
|
+
CREATE TABLE booking_notes (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
booking_id TEXT NOT NULL,
|
|
97
|
+
author_email TEXT NOT NULL,
|
|
98
|
+
content TEXT NOT NULL,
|
|
99
|
+
created_at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
const db = drizzle(client, { schema });
|
|
104
|
+
currentUserEmail = HOST_EMAIL;
|
|
105
|
+
setSchedulingContext({
|
|
106
|
+
getDb: () => db,
|
|
107
|
+
schema,
|
|
108
|
+
getCurrentUserEmail: () => currentUserEmail,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
await client.execute({
|
|
113
|
+
sql: `INSERT INTO bookings (
|
|
114
|
+
id, uid, event_type_id, host_email, title, start_time, end_time,
|
|
115
|
+
timezone, status, cancel_token, reschedule_token, ical_uid, created_at, updated_at
|
|
116
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
117
|
+
args: [
|
|
118
|
+
"booking-1",
|
|
119
|
+
BOOKING_UID,
|
|
120
|
+
"event-type-1",
|
|
121
|
+
HOST_EMAIL,
|
|
122
|
+
"Test Meeting",
|
|
123
|
+
"2026-08-01T10:00:00.000Z",
|
|
124
|
+
"2026-08-01T10:30:00.000Z",
|
|
125
|
+
"UTC",
|
|
126
|
+
"pending",
|
|
127
|
+
"cancel-token-1",
|
|
128
|
+
"reschedule-token-1",
|
|
129
|
+
"ical-uid-1",
|
|
130
|
+
now,
|
|
131
|
+
now,
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
await client.execute({
|
|
135
|
+
sql: `INSERT INTO booking_attendees (id, booking_id, email, name, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
136
|
+
args: ["attendee-1", "booking-1", ATTENDEE_EMAIL, "Attendee One", now],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
client.close();
|
|
142
|
+
rmSync(dbDir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
async function bookingStatus(): Promise<string> {
|
|
146
|
+
const { rows } = await client.execute({
|
|
147
|
+
sql: "SELECT status FROM bookings WHERE uid = ?",
|
|
148
|
+
args: [BOOKING_UID],
|
|
149
|
+
});
|
|
150
|
+
return String(rows[0]?.status);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function attendeeEmails(): Promise<string[]> {
|
|
154
|
+
const { rows } = await client.execute({
|
|
155
|
+
sql: "SELECT email FROM booking_attendees WHERE booking_id = 'booking-1'",
|
|
156
|
+
});
|
|
157
|
+
return rows.map((r: any) => String(r.email)).sort();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function attendeeNoShow(email: string): Promise<boolean> {
|
|
161
|
+
const { rows } = await client.execute({
|
|
162
|
+
sql: "SELECT no_show FROM booking_attendees WHERE booking_id = 'booking-1' AND email = ?",
|
|
163
|
+
args: [email],
|
|
164
|
+
});
|
|
165
|
+
return Boolean(rows[0]?.no_show);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function noteCount(): Promise<number> {
|
|
169
|
+
const { rows } = await client.execute("SELECT * FROM booking_notes");
|
|
170
|
+
return rows.length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
describe("confirm-booking authorization", () => {
|
|
174
|
+
it("rejects a non-host caller and leaves the booking unchanged", async () => {
|
|
175
|
+
currentUserEmail = OUTSIDER_EMAIL;
|
|
176
|
+
await expect(confirmBooking.run({ uid: BOOKING_UID })).rejects.toThrow(
|
|
177
|
+
/not authorized/i,
|
|
178
|
+
);
|
|
179
|
+
expect(await bookingStatus()).toBe("pending");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("allows the host to confirm the booking", async () => {
|
|
183
|
+
currentUserEmail = HOST_EMAIL;
|
|
184
|
+
const result: any = await confirmBooking.run({ uid: BOOKING_UID });
|
|
185
|
+
expect(result.booking?.status).toBe("confirmed");
|
|
186
|
+
expect(await bookingStatus()).toBe("confirmed");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("mark-no-show authorization", () => {
|
|
191
|
+
it("rejects a non-host caller and leaves the attendee unchanged", async () => {
|
|
192
|
+
currentUserEmail = OUTSIDER_EMAIL;
|
|
193
|
+
await expect(
|
|
194
|
+
markNoShow.run({ uid: BOOKING_UID, attendeeEmail: ATTENDEE_EMAIL }),
|
|
195
|
+
).rejects.toThrow(/not authorized/i);
|
|
196
|
+
expect(await attendeeNoShow(ATTENDEE_EMAIL)).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("allows the host to mark an attendee no-show", async () => {
|
|
200
|
+
currentUserEmail = HOST_EMAIL;
|
|
201
|
+
const result: any = await markNoShow.run({
|
|
202
|
+
uid: BOOKING_UID,
|
|
203
|
+
attendeeEmail: ATTENDEE_EMAIL,
|
|
204
|
+
});
|
|
205
|
+
expect(result.ok).toBe(true);
|
|
206
|
+
expect(await attendeeNoShow(ATTENDEE_EMAIL)).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("add-booking-attendee authorization", () => {
|
|
211
|
+
it("rejects a non-host caller and does not add the attendee", async () => {
|
|
212
|
+
currentUserEmail = OUTSIDER_EMAIL;
|
|
213
|
+
await expect(
|
|
214
|
+
addBookingAttendee.run({
|
|
215
|
+
uid: BOOKING_UID,
|
|
216
|
+
name: "Intruder",
|
|
217
|
+
email: "intruder@example.com",
|
|
218
|
+
}),
|
|
219
|
+
).rejects.toThrow(/not authorized/i);
|
|
220
|
+
expect(await attendeeEmails()).toEqual([ATTENDEE_EMAIL]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("allows the host to add an attendee", async () => {
|
|
224
|
+
currentUserEmail = HOST_EMAIL;
|
|
225
|
+
await addBookingAttendee.run({
|
|
226
|
+
uid: BOOKING_UID,
|
|
227
|
+
name: "New Guest",
|
|
228
|
+
email: "guest@example.com",
|
|
229
|
+
});
|
|
230
|
+
expect(await attendeeEmails()).toEqual(
|
|
231
|
+
[ATTENDEE_EMAIL, "guest@example.com"].sort(),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("remove-booking-attendee authorization", () => {
|
|
237
|
+
it("rejects a non-host caller and does not remove the attendee", async () => {
|
|
238
|
+
currentUserEmail = OUTSIDER_EMAIL;
|
|
239
|
+
await expect(
|
|
240
|
+
removeBookingAttendee.run({ uid: BOOKING_UID, email: ATTENDEE_EMAIL }),
|
|
241
|
+
).rejects.toThrow(/not authorized/i);
|
|
242
|
+
expect(await attendeeEmails()).toEqual([ATTENDEE_EMAIL]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("allows the host to remove an attendee", async () => {
|
|
246
|
+
currentUserEmail = HOST_EMAIL;
|
|
247
|
+
await removeBookingAttendee.run({
|
|
248
|
+
uid: BOOKING_UID,
|
|
249
|
+
email: ATTENDEE_EMAIL,
|
|
250
|
+
});
|
|
251
|
+
expect(await attendeeEmails()).toEqual([]);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("add-booking-note authorization", () => {
|
|
256
|
+
it("rejects a non-host caller and does not create a note", async () => {
|
|
257
|
+
currentUserEmail = OUTSIDER_EMAIL;
|
|
258
|
+
await expect(
|
|
259
|
+
addBookingNote.run({ uid: BOOKING_UID, content: "snooping" }),
|
|
260
|
+
).rejects.toThrow(/not authorized/i);
|
|
261
|
+
expect(await noteCount()).toBe(0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("allows the host to add a note", async () => {
|
|
265
|
+
currentUserEmail = HOST_EMAIL;
|
|
266
|
+
const result: any = await addBookingNote.run({
|
|
267
|
+
uid: BOOKING_UID,
|
|
268
|
+
content: "host note",
|
|
269
|
+
});
|
|
270
|
+
expect(result.id).toBeTruthy();
|
|
271
|
+
expect(await noteCount()).toBe(1);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("send-reschedule-link authorization", () => {
|
|
276
|
+
it("rejects a non-host caller and never returns the reschedule token", async () => {
|
|
277
|
+
currentUserEmail = OUTSIDER_EMAIL;
|
|
278
|
+
await expect(sendRescheduleLink.run({ uid: BOOKING_UID })).rejects.toThrow(
|
|
279
|
+
/not authorized/i,
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("returns the reschedule URL with token for the host", async () => {
|
|
284
|
+
currentUserEmail = HOST_EMAIL;
|
|
285
|
+
const result: any = await sendRescheduleLink.run({ uid: BOOKING_UID });
|
|
286
|
+
expect(result.url).toContain("reschedule-token-1");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -5,11 +5,15 @@ import {
|
|
|
5
5
|
updateBookingStatus,
|
|
6
6
|
getBookingByUid,
|
|
7
7
|
} from "../server/bookings-repo.js";
|
|
8
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
8
9
|
|
|
9
10
|
export default defineAction({
|
|
10
11
|
description: "Confirm a pending booking (requires-confirmation flow)",
|
|
11
12
|
schema: z.object({ uid: z.string() }),
|
|
12
13
|
run: async (args) => {
|
|
14
|
+
const booking = await getBookingByUid(args.uid);
|
|
15
|
+
if (!booking) throw new Error(`Booking ${args.uid} not found`);
|
|
16
|
+
assertBookingHost(booking, "confirm this booking");
|
|
13
17
|
await updateBookingStatus(args.uid, "confirmed");
|
|
14
18
|
return { booking: await getBookingByUid(args.uid) };
|
|
15
19
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
import {
|
|
@@ -17,6 +18,7 @@ export default defineAction({
|
|
|
17
18
|
run: async (args) => {
|
|
18
19
|
const source = await getEventTypeById(args.id);
|
|
19
20
|
if (!source) throw new Error("Event type not found");
|
|
21
|
+
await assertAccess("event-type", args.id, "editor");
|
|
20
22
|
return {
|
|
21
23
|
eventType: await createEventType({
|
|
22
24
|
ownerEmail: source.teamId ? undefined : currentUserEmail(),
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tenant write (IDOR) regression tests for event-type-scoped actions
|
|
3
|
+
* that mutate a resource owned by another tenant: revoking a private hashed
|
|
4
|
+
* link and duplicating an event type. Mirrors the `assertAccess("event-type",
|
|
5
|
+
* ..., <role>)` guard already used by `add-private-link.ts` /
|
|
6
|
+
* `delete-event-type.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getRequestUserEmail,
|
|
15
|
+
runWithRequestContext,
|
|
16
|
+
} from "@agent-native/core/server/request-context";
|
|
17
|
+
import {
|
|
18
|
+
ForbiddenError,
|
|
19
|
+
registerShareableResource,
|
|
20
|
+
} from "@agent-native/core/sharing";
|
|
21
|
+
import { createClient, type Client } from "@libsql/client";
|
|
22
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
23
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
24
|
+
|
|
25
|
+
import * as schema from "../schema/index.js";
|
|
26
|
+
import { setSchedulingContext } from "../server/context.js";
|
|
27
|
+
import duplicateEventType from "./duplicate-event-type.js";
|
|
28
|
+
import revokePrivateLink from "./revoke-private-link.js";
|
|
29
|
+
|
|
30
|
+
const OWNER_EMAIL = "owner@example.com";
|
|
31
|
+
const OUTSIDER_EMAIL = "outsider@example.com";
|
|
32
|
+
const EVENT_TYPE_ID = "event-type-1";
|
|
33
|
+
const HASH = "private-link-hash-1";
|
|
34
|
+
|
|
35
|
+
let client: Client;
|
|
36
|
+
let dbDir: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
dbDir = mkdtempSync(join(tmpdir(), "scheduling-eventtype-authz-test-"));
|
|
40
|
+
client = createClient({ url: `file:${join(dbDir, `${randomUUID()}.db`)}` });
|
|
41
|
+
await client.execute(`
|
|
42
|
+
CREATE TABLE event_types (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
title TEXT NOT NULL,
|
|
45
|
+
slug TEXT NOT NULL,
|
|
46
|
+
description TEXT,
|
|
47
|
+
length INTEGER NOT NULL DEFAULT 30,
|
|
48
|
+
durations TEXT,
|
|
49
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
hidden INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
color TEXT,
|
|
52
|
+
scheduling_type TEXT NOT NULL DEFAULT 'personal',
|
|
53
|
+
team_id TEXT,
|
|
54
|
+
locations TEXT,
|
|
55
|
+
custom_fields TEXT,
|
|
56
|
+
schedule_id TEXT,
|
|
57
|
+
minimum_booking_notice INTEGER NOT NULL DEFAULT 0,
|
|
58
|
+
before_event_buffer INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
after_event_buffer INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
slot_interval INTEGER,
|
|
61
|
+
period_type TEXT NOT NULL DEFAULT 'rolling',
|
|
62
|
+
period_days INTEGER DEFAULT 60,
|
|
63
|
+
period_start_date TEXT,
|
|
64
|
+
period_end_date TEXT,
|
|
65
|
+
seats_per_time_slot INTEGER,
|
|
66
|
+
requires_confirmation INTEGER NOT NULL DEFAULT 0,
|
|
67
|
+
disable_guests INTEGER NOT NULL DEFAULT 0,
|
|
68
|
+
hide_calendar_notes INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
success_redirect_url TEXT,
|
|
70
|
+
booking_limits TEXT,
|
|
71
|
+
lock_time_zone_toggle INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
recurring_event TEXT,
|
|
73
|
+
event_name TEXT,
|
|
74
|
+
metadata TEXT,
|
|
75
|
+
created_at TEXT NOT NULL,
|
|
76
|
+
updated_at TEXT NOT NULL,
|
|
77
|
+
owner_email TEXT NOT NULL DEFAULT 'local@localhost',
|
|
78
|
+
org_id TEXT,
|
|
79
|
+
visibility TEXT NOT NULL DEFAULT 'private'
|
|
80
|
+
);
|
|
81
|
+
`);
|
|
82
|
+
await client.execute(`
|
|
83
|
+
CREATE TABLE hashed_links (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
hash TEXT NOT NULL UNIQUE,
|
|
86
|
+
event_type_id TEXT NOT NULL,
|
|
87
|
+
expires_at TEXT,
|
|
88
|
+
is_single_use INTEGER NOT NULL DEFAULT 0,
|
|
89
|
+
used_at TEXT,
|
|
90
|
+
created_at TEXT NOT NULL
|
|
91
|
+
);
|
|
92
|
+
`);
|
|
93
|
+
await client.execute(`
|
|
94
|
+
CREATE TABLE event_type_shares (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
resource_id TEXT NOT NULL,
|
|
97
|
+
principal_type TEXT NOT NULL,
|
|
98
|
+
principal_id TEXT NOT NULL,
|
|
99
|
+
role TEXT NOT NULL DEFAULT 'viewer',
|
|
100
|
+
created_by TEXT NOT NULL,
|
|
101
|
+
created_at TEXT NOT NULL
|
|
102
|
+
);
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
const db = drizzle(client, { schema });
|
|
106
|
+
setSchedulingContext({
|
|
107
|
+
getDb: () => db,
|
|
108
|
+
schema,
|
|
109
|
+
// Mirrors real app wiring (templates/scheduling's server plugin): the
|
|
110
|
+
// scheduling package's own "current user" and the framework's
|
|
111
|
+
// request-context ALS (used by `assertAccess`) both resolve to the same
|
|
112
|
+
// identity.
|
|
113
|
+
getCurrentUserEmail: () => getRequestUserEmail(),
|
|
114
|
+
});
|
|
115
|
+
registerShareableResource({
|
|
116
|
+
type: "event-type",
|
|
117
|
+
resourceTable: schema.eventTypes,
|
|
118
|
+
sharesTable: schema.eventTypeShares,
|
|
119
|
+
displayName: "Event Type",
|
|
120
|
+
getDb: () => db,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
await client.execute({
|
|
125
|
+
sql: `INSERT INTO event_types (
|
|
126
|
+
id, title, slug, length, created_at, updated_at, owner_email, visibility
|
|
127
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
128
|
+
args: [
|
|
129
|
+
EVENT_TYPE_ID,
|
|
130
|
+
"30 Min Meeting",
|
|
131
|
+
"30min",
|
|
132
|
+
30,
|
|
133
|
+
now,
|
|
134
|
+
now,
|
|
135
|
+
OWNER_EMAIL,
|
|
136
|
+
"private",
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
await client.execute({
|
|
140
|
+
sql: `INSERT INTO hashed_links (id, hash, event_type_id, created_at) VALUES (?, ?, ?, ?)`,
|
|
141
|
+
args: ["link-1", HASH, EVENT_TYPE_ID, now],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterEach(() => {
|
|
146
|
+
client.close();
|
|
147
|
+
rmSync(dbDir, { recursive: true, force: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
async function hashedLinkExists(): Promise<boolean> {
|
|
151
|
+
const { rows } = await client.execute({
|
|
152
|
+
sql: "SELECT 1 FROM hashed_links WHERE hash = ?",
|
|
153
|
+
args: [HASH],
|
|
154
|
+
});
|
|
155
|
+
return rows.length > 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function eventTypeCount(): Promise<number> {
|
|
159
|
+
const { rows } = await client.execute("SELECT * FROM event_types");
|
|
160
|
+
return rows.length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
describe("revoke-private-link authorization", () => {
|
|
164
|
+
it("returns the same idempotent result for an inaccessible link and keeps it", async () => {
|
|
165
|
+
const result: any = await runWithRequestContext(
|
|
166
|
+
{ userEmail: OUTSIDER_EMAIL },
|
|
167
|
+
() => revokePrivateLink.run({ hash: HASH }),
|
|
168
|
+
);
|
|
169
|
+
expect(result.ok).toBe(true);
|
|
170
|
+
expect(await hashedLinkExists()).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("allows the owning event type's editor (the owner) to revoke the link", async () => {
|
|
174
|
+
const result: any = await runWithRequestContext(
|
|
175
|
+
{ userEmail: OWNER_EMAIL },
|
|
176
|
+
() => revokePrivateLink.run({ hash: HASH }),
|
|
177
|
+
);
|
|
178
|
+
expect(result.ok).toBe(true);
|
|
179
|
+
expect(await hashedLinkExists()).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("is idempotent for an unknown hash without requiring access", async () => {
|
|
183
|
+
const result: any = await runWithRequestContext(
|
|
184
|
+
{ userEmail: OUTSIDER_EMAIL },
|
|
185
|
+
() => revokePrivateLink.run({ hash: "does-not-exist" }),
|
|
186
|
+
);
|
|
187
|
+
expect(result.ok).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("duplicate-event-type authorization", () => {
|
|
192
|
+
it("rejects a caller with no access to the source event type and does not duplicate it", async () => {
|
|
193
|
+
await expect(
|
|
194
|
+
runWithRequestContext({ userEmail: OUTSIDER_EMAIL }, () =>
|
|
195
|
+
duplicateEventType.run({ id: EVENT_TYPE_ID, newSlug: "copy" }),
|
|
196
|
+
),
|
|
197
|
+
).rejects.toBeInstanceOf(ForbiddenError);
|
|
198
|
+
expect(await eventTypeCount()).toBe(1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("allows the owner to duplicate their own event type", async () => {
|
|
202
|
+
const result: any = await runWithRequestContext(
|
|
203
|
+
{ userEmail: OWNER_EMAIL },
|
|
204
|
+
() => duplicateEventType.run({ id: EVENT_TYPE_ID, newSlug: "copy" }),
|
|
205
|
+
);
|
|
206
|
+
expect(result.eventType?.slug).toBe("copy");
|
|
207
|
+
expect(await eventTypeCount()).toBe(2);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -2,9 +2,15 @@ import { defineAction } from "@agent-native/core";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
5
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
5
6
|
|
|
6
7
|
export default defineAction({
|
|
7
8
|
description: "Get a booking by uid",
|
|
8
9
|
schema: z.object({ uid: z.string() }),
|
|
9
|
-
run: async (args) =>
|
|
10
|
+
run: async (args) => {
|
|
11
|
+
const booking = await getBookingByUid(args.uid);
|
|
12
|
+
if (!booking) return { booking: null };
|
|
13
|
+
assertBookingHost(booking, "view this booking");
|
|
14
|
+
return { booking };
|
|
15
|
+
},
|
|
10
16
|
});
|
|
@@ -2,6 +2,8 @@ import { defineAction } from "@agent-native/core";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import { markNoShow } from "../server/booking-service.js";
|
|
5
|
+
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
6
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
5
7
|
|
|
6
8
|
export default defineAction({
|
|
7
9
|
description: "Mark an attendee as no-show on a booking",
|
|
@@ -10,6 +12,9 @@ export default defineAction({
|
|
|
10
12
|
attendeeEmail: z.string(),
|
|
11
13
|
}),
|
|
12
14
|
run: async (args) => {
|
|
15
|
+
const booking = await getBookingByUid(args.uid);
|
|
16
|
+
if (!booking) throw new Error(`Booking ${args.uid} not found`);
|
|
17
|
+
assertBookingHost(booking, "mark no-show on this booking");
|
|
13
18
|
await markNoShow(args.uid, args.attendeeEmail);
|
|
14
19
|
return { ok: true };
|
|
15
20
|
},
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
|
|
5
5
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
6
6
|
import { getSchedulingContext } from "../server/context.js";
|
|
7
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
7
8
|
|
|
8
9
|
export default defineAction({
|
|
9
10
|
description: "Remove an attendee from a booking",
|
|
@@ -11,6 +12,7 @@ export default defineAction({
|
|
|
11
12
|
run: async (args) => {
|
|
12
13
|
const booking = await getBookingByUid(args.uid);
|
|
13
14
|
if (!booking) throw new Error(`Booking ${args.uid} not found`);
|
|
15
|
+
assertBookingHost(booking, "remove an attendee from this booking");
|
|
14
16
|
const { getDb, schema } = getSchedulingContext();
|
|
15
17
|
await getDb()
|
|
16
18
|
.delete(schema.bookingAttendees)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
2
3
|
import { eq } from "drizzle-orm";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
@@ -9,7 +10,21 @@ export default defineAction({
|
|
|
9
10
|
schema: z.object({ hash: z.string() }),
|
|
10
11
|
run: async (args) => {
|
|
11
12
|
const { getDb, schema } = getSchedulingContext();
|
|
12
|
-
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const [link] = await db
|
|
15
|
+
.select({ eventTypeId: schema.hashedLinks.eventTypeId })
|
|
16
|
+
.from(schema.hashedLinks)
|
|
17
|
+
.where(eq(schema.hashedLinks.hash, args.hash))
|
|
18
|
+
.limit(1);
|
|
19
|
+
if (!link) return { ok: true };
|
|
20
|
+
try {
|
|
21
|
+
await assertAccess("event-type", link.eventTypeId, "editor");
|
|
22
|
+
} catch {
|
|
23
|
+
// Keep private-link hashes unprobeable: callers without access receive
|
|
24
|
+
// the same idempotent result as callers presenting an unknown hash.
|
|
25
|
+
return { ok: true };
|
|
26
|
+
}
|
|
27
|
+
await db
|
|
13
28
|
.delete(schema.hashedLinks)
|
|
14
29
|
.where(eq(schema.hashedLinks.hash, args.hash));
|
|
15
30
|
return { ok: true };
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
import { getBookingByUid } from "../server/bookings-repo.js";
|
|
5
5
|
import { getSchedulingContext } from "../server/context.js";
|
|
6
|
+
import { assertBookingHost } from "./_helpers.js";
|
|
6
7
|
|
|
7
8
|
export default defineAction({
|
|
8
9
|
description:
|
|
@@ -11,6 +12,7 @@ export default defineAction({
|
|
|
11
12
|
run: async (args) => {
|
|
12
13
|
const booking = await getBookingByUid(args.uid);
|
|
13
14
|
if (!booking) throw new Error(`Booking ${args.uid} not found`);
|
|
15
|
+
assertBookingHost(booking, "view the reschedule link for this booking");
|
|
14
16
|
const baseUrl = getSchedulingContext().publicBaseUrl ?? "";
|
|
15
17
|
return {
|
|
16
18
|
url: `${baseUrl}/reschedule/${booking.uid}?token=${booking.rescheduleToken}`,
|