@agent-native/core 0.192.0 → 0.192.1-nightly-20260926144920
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/actions/cancel-booking.ts +12 -4
- package/corpus/templates/calendar/actions/list-bookings.ts +23 -2
- package/corpus/templates/calendar/actions/list-events.ts +42 -29
- package/corpus/templates/calendar/actions/update-booking-link.ts +27 -1
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +9 -2
- package/corpus/templates/calendar/app/i18n/en-US.ts +8 -0
- package/corpus/templates/calendar/app/lib/booking-sorting.ts +7 -0
- package/corpus/templates/calendar/app/lib/event-colors.ts +29 -4
- package/corpus/templates/calendar/app/pages/BookingsList.tsx +99 -19
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +5 -11
- package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +5 -0
- package/corpus/templates/calendar/changelog/2026-09-25-ambiguous-zoom-bookings-need-review.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-bookings-list-shows-newest-bookings-first.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-canceling-a-booking-now-resolves-its-zoom-meeting-first.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-colleague-events-use-sidebar-colors.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-public-booking-pages-reject-malformed-meeting-settings-befor.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-shared-booking-cancellations-require-editor-access-and-unres.md +6 -0
- package/corpus/templates/calendar/server/db/schema.ts +5 -0
- package/corpus/templates/calendar/server/handlers/availability.ts +32 -29
- package/corpus/templates/calendar/server/handlers/bookings.ts +189 -62
- package/corpus/templates/calendar/server/lib/booking-link-utils.ts +46 -1
- package/corpus/templates/calendar/server/lib/zoom.ts +66 -1
- package/corpus/templates/calendar/server/plugins/db.ts +13 -0
- package/corpus/templates/calendar/shared/api.ts +4 -0
- package/corpus/templates/clips/app/components/capture-install-options.tsx +9 -6
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +4 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -4
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +4 -0
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +7 -33
- package/corpus/templates/clips/changelog/2026-09-26-desktop-platform-icon.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-state-now-explains-how-screen-recordings-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-view-has-a-clearer-headline-and-a-focused.md +6 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +74 -40
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +64 -42
- package/corpus/templates/design/app/components/design/design-canvas/creation.ts +1 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -2
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +56 -25
- package/corpus/templates/design/app/routes/design-systems.tsx +2 -4
- package/corpus/templates/design/app/routes/design.$id.tsx +2 -1
- package/corpus/templates/design/app/routes/public-design-meta.ts +0 -39
- package/corpus/templates/design/app/routes/templates.tsx +2 -4
- package/corpus/templates/design/app/routes/visual-edit_.$id.tsx +2 -1
- package/corpus/templates/design/changelog/2026-09-25-escape-finishes-open-pen-paths.md +7 -0
- package/corpus/templates/design/changelog/2026-09-25-pen-paths-remain-available-after-save-failures-and-retries-k.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-pen-retries-keep-extending-the-selected-vector-after-a-save-.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-pressing-enter-finishes-a-new-pen-path-and-selects-its-vector-on-move.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-cmd-d-places-screen-duplicates-in-the-next-open-slot.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-dropping-a-layer-beside-a-sibling-keeps-that-insertion-point.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-layers-dragged-out-of-frames-now-land-at-the-board-level-and.md +6 -0
- package/corpus/templates/design/changelog/2026-09-26-opening-shared-designs-no-longer-fails-during-client-startup.md +6 -0
- package/corpus/templates/design/package.json +1 -1
- package/corpus/templates/design/scripts/check-client-server-boundary.ts +55 -0
- package/corpus/templates/design/server/lib/public-design-meta.server.ts +40 -0
- package/corpus/templates/mail/app/components/email/EmailListItem.tsx +11 -9
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +9 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
- package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +25 -2
- package/corpus/templates/mail/app/hooks/use-emails.ts +159 -27
- package/corpus/templates/mail/app/i18n/en-US.ts +2 -1
- package/corpus/templates/mail/app/lib/label-display.ts +6 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +5 -0
- package/corpus/templates/mail/changelog/2026-09-26-fixed-importance-actions-label-display-and-triage-loading-fe.md +6 -0
- package/corpus/templates/mail/changelog/2026-09-26-mail-cancels-stale-thread-read-cooldown-retries-after-a-newe.md +6 -0
- package/corpus/templates/mail/server/lib/google-api.ts +12 -7
- package/corpus/templates/slides/app/i18n/en-US.ts +0 -1
- package/corpus/templates/slides/app/pages/Index.tsx +1 -29
- package/corpus/templates/slides/changelog/2026-09-26-create-decks-from-the-prompt-without-a-separate-new-deck-button.md +6 -0
- package/corpus/templates/tasks/.agents/skills/agent-watchdog/SKILL.md +7 -2
- package/dist/client/settings/UsageSection.js +18 -5
- package/dist/client/settings/recent-prompt-groups.d.ts +12 -0
- package/dist/client/settings/recent-prompt-groups.js +20 -0
- package/dist/client/use-action.js +7 -0
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-routes.js +25 -2
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/package.json +3 -3
|
@@ -75,36 +75,38 @@ export const getPublicAvailability = defineEventHandler(
|
|
|
75
75
|
.from(schema.bookingLinks)
|
|
76
76
|
.where(eq(schema.bookingLinks.slug, slug))
|
|
77
77
|
.then((rows) => rows[0]);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
78
|
+
const usernameOwnerEmail = username
|
|
79
|
+
? await getBookingUsernameOwner(username)
|
|
80
|
+
: null;
|
|
81
|
+
if (
|
|
82
|
+
username &&
|
|
83
|
+
(!usernameOwnerEmail ||
|
|
84
|
+
(link && link.ownerEmail !== usernameOwnerEmail))
|
|
85
|
+
) {
|
|
86
|
+
throw createError({
|
|
87
|
+
statusCode: 404,
|
|
88
|
+
statusMessage: "Booking page not found",
|
|
89
|
+
});
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!ownerEmail) {
|
|
96
|
-
throw createError({
|
|
97
|
-
statusCode: 404,
|
|
98
|
-
statusMessage: "Booking page not found",
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
92
|
+
const ownerEmail = link?.ownerEmail || usernameOwnerEmail;
|
|
93
|
+
if (ownerEmail) {
|
|
102
94
|
const ownerConfig = (await getUserSetting(
|
|
103
95
|
ownerEmail,
|
|
104
96
|
"calendar-availability",
|
|
105
97
|
)) as unknown as AvailabilityConfig | null;
|
|
106
|
-
if (
|
|
107
|
-
|
|
98
|
+
if (
|
|
99
|
+
ownerConfig &&
|
|
100
|
+
(link?.ownerEmail ||
|
|
101
|
+
!username ||
|
|
102
|
+
ownerConfig.bookingPageSlug === slug)
|
|
103
|
+
) {
|
|
104
|
+
return ownerConfig;
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
!ownerConfig &&
|
|
108
|
+
(link?.ownerEmail || !username || slug === "book")
|
|
109
|
+
) {
|
|
108
110
|
const ownerSettings = (await getUserSetting(
|
|
109
111
|
ownerEmail,
|
|
110
112
|
"calendar-settings",
|
|
@@ -113,11 +115,12 @@ export const getPublicAvailability = defineEventHandler(
|
|
|
113
115
|
ownerSettings?.timezone || "America/New_York",
|
|
114
116
|
);
|
|
115
117
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
if (username) {
|
|
119
|
+
throw createError({
|
|
120
|
+
statusCode: 404,
|
|
121
|
+
statusMessage: "Booking page not found",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -3,6 +3,7 @@ import { getOrgContext, orgMembers } from "@agent-native/core/org";
|
|
|
3
3
|
import {
|
|
4
4
|
getSession,
|
|
5
5
|
getAppProductionUrl,
|
|
6
|
+
getRequestContext,
|
|
6
7
|
recordChange,
|
|
7
8
|
readBody,
|
|
8
9
|
runWithRequestContext,
|
|
@@ -29,7 +30,6 @@ import type {
|
|
|
29
30
|
Booking,
|
|
30
31
|
CalendarEvent,
|
|
31
32
|
AvailabilityConfig,
|
|
32
|
-
ConferencingConfig,
|
|
33
33
|
CustomField,
|
|
34
34
|
TimeSlot,
|
|
35
35
|
} from "../../shared/api.js";
|
|
@@ -55,12 +55,18 @@ import {
|
|
|
55
55
|
import {
|
|
56
56
|
getBookingLinkCoHostEmails,
|
|
57
57
|
getBookingLinkRequiredHostEmails,
|
|
58
|
+
isBookingLinkHost,
|
|
58
59
|
normalizeBookingHosts,
|
|
60
|
+
parseBookingConferencingConfig,
|
|
59
61
|
} from "../lib/booking-link-utils.js";
|
|
60
62
|
import { getOwnerBookingTimeZone } from "../lib/booking-timezone.js";
|
|
61
63
|
import { eventBlocksAvailability } from "../lib/calendar-availability.js";
|
|
62
64
|
import * as googleCalendar from "../lib/google-calendar.js";
|
|
63
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
createZoomMeeting,
|
|
67
|
+
deleteZoomMeeting,
|
|
68
|
+
needsZoomCancellationReview,
|
|
69
|
+
} from "../lib/zoom.js";
|
|
64
70
|
import { getBookingUsernameOwner } from "./booking-usernames.js";
|
|
65
71
|
|
|
66
72
|
async function requireRequestContext<T>(
|
|
@@ -89,16 +95,23 @@ async function getBookingLinkSlugsForOwners(
|
|
|
89
95
|
return Array.from(new Set(rows.map((row) => row.slug)));
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
async function
|
|
93
|
-
slug: string,
|
|
94
|
-
): Promise<string | undefined> {
|
|
98
|
+
async function getBookingLinkDetails(slug: string) {
|
|
95
99
|
if (!slug) return undefined;
|
|
96
|
-
|
|
97
|
-
.select({
|
|
100
|
+
return getDb()
|
|
101
|
+
.select({
|
|
102
|
+
ownerEmail: schema.bookingLinks.ownerEmail,
|
|
103
|
+
hosts: schema.bookingLinks.hosts,
|
|
104
|
+
conferencing: schema.bookingLinks.conferencing,
|
|
105
|
+
})
|
|
98
106
|
.from(schema.bookingLinks)
|
|
99
107
|
.where(eq(schema.bookingLinks.slug, slug))
|
|
100
108
|
.then((rows) => rows[0]);
|
|
101
|
-
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function getBookingLinkOwnerEmail(
|
|
112
|
+
slug: string,
|
|
113
|
+
): Promise<string | undefined> {
|
|
114
|
+
return (await getBookingLinkDetails(slug))?.ownerEmail;
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
function stripCrlf(value: unknown): string {
|
|
@@ -608,7 +621,7 @@ async function resolveAvailabilityContext({
|
|
|
608
621
|
draft?: BookingAvailabilityDraft;
|
|
609
622
|
db?: ConflictDb;
|
|
610
623
|
}): Promise<AvailabilityContext> {
|
|
611
|
-
const [configRaw, bookingLink] = await Promise.all([
|
|
624
|
+
const [configRaw, bookingLink, usernameOwnerEmail] = await Promise.all([
|
|
612
625
|
getSetting("calendar-availability"),
|
|
613
626
|
slug
|
|
614
627
|
? db
|
|
@@ -629,6 +642,9 @@ async function resolveAvailabilityContext({
|
|
|
629
642
|
)
|
|
630
643
|
.then((rows) => rows[0])
|
|
631
644
|
: Promise.resolve(undefined),
|
|
645
|
+
username && !draft
|
|
646
|
+
? getBookingUsernameOwner(username)
|
|
647
|
+
: Promise.resolve(null),
|
|
632
648
|
]);
|
|
633
649
|
if (draft && !bookingLink) {
|
|
634
650
|
throw createError({
|
|
@@ -636,47 +652,57 @@ async function resolveAvailabilityContext({
|
|
|
636
652
|
statusMessage: "Booking link not found",
|
|
637
653
|
});
|
|
638
654
|
}
|
|
655
|
+
if (username && !draft && !usernameOwnerEmail) {
|
|
656
|
+
throw createError({
|
|
657
|
+
statusCode: 404,
|
|
658
|
+
statusMessage: "Booking page not found",
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (username && !draft && bookingLink) {
|
|
662
|
+
if (bookingLink.ownerEmail !== usernameOwnerEmail) {
|
|
663
|
+
throw createError({
|
|
664
|
+
statusCode: 404,
|
|
665
|
+
statusMessage: "Booking page not found",
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
} else if (username && !draft) {
|
|
669
|
+
return {
|
|
670
|
+
effectiveConfig: null,
|
|
671
|
+
ownerEmail: undefined,
|
|
672
|
+
hostEmails: [],
|
|
673
|
+
eligibleHosts: [],
|
|
674
|
+
slug,
|
|
675
|
+
bookingLink: undefined,
|
|
676
|
+
durationSource: undefined,
|
|
677
|
+
conflictSlugs: [],
|
|
678
|
+
};
|
|
679
|
+
}
|
|
639
680
|
const config = configRaw as unknown as AvailabilityConfig | null;
|
|
640
|
-
const
|
|
641
|
-
!bookingLink && username && !draft
|
|
642
|
-
? await getBookingUsernameOwner(username)
|
|
643
|
-
: null;
|
|
644
|
-
const candidateOwnerEmail = bookingLink?.ownerEmail || usernameOwnerEmail;
|
|
645
|
-
const [candidateOwnerConfigRaw, candidateOwnerSettingsRaw] =
|
|
646
|
-
candidateOwnerEmail
|
|
647
|
-
? await Promise.all([
|
|
648
|
-
getUserSetting(candidateOwnerEmail, "calendar-availability"),
|
|
649
|
-
getUserSetting(candidateOwnerEmail, "calendar-settings"),
|
|
650
|
-
])
|
|
651
|
-
: [null, null];
|
|
652
|
-
const candidateOwnerConfig =
|
|
653
|
-
candidateOwnerConfigRaw as AvailabilityConfig | null;
|
|
654
|
-
const usernameSlugMatches =
|
|
655
|
-
!usernameOwnerEmail ||
|
|
656
|
-
candidateOwnerConfig?.bookingPageSlug === slug ||
|
|
657
|
-
(!candidateOwnerConfig && slug === "book");
|
|
658
|
-
const ownerEmail =
|
|
659
|
-
bookingLink?.ownerEmail ||
|
|
660
|
-
(usernameSlugMatches ? usernameOwnerEmail || undefined : undefined);
|
|
681
|
+
const ownerEmail = bookingLink?.ownerEmail;
|
|
661
682
|
const overrides = bookingLink
|
|
662
683
|
? resolveBookingLinkAvailabilityOverrides({ bookingLink, draft })
|
|
663
684
|
: undefined;
|
|
664
685
|
const hostEmails = overrides?.hostEmails ?? (ownerEmail ? [ownerEmail] : []);
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
686
|
+
const [ownerConfigRaw, ownerSettingsRaw, ownerLinkSlugs, eligibleHosts] =
|
|
687
|
+
await Promise.all([
|
|
688
|
+
ownerEmail
|
|
689
|
+
? getUserSetting(ownerEmail, "calendar-availability")
|
|
690
|
+
: Promise.resolve(null),
|
|
691
|
+
ownerEmail
|
|
692
|
+
? getUserSetting(ownerEmail, "calendar-settings")
|
|
693
|
+
: Promise.resolve(null),
|
|
694
|
+
ownerEmail
|
|
695
|
+
? getBookingLinkSlugsForOwners(hostEmails, db)
|
|
696
|
+
: Promise.resolve([]),
|
|
697
|
+
getEligibleHostAvailability(ownerEmail, hostEmails),
|
|
698
|
+
]);
|
|
699
|
+
const ownerConfig = ownerConfigRaw as AvailabilityConfig | null;
|
|
700
|
+
const ownerSettings = ownerSettingsRaw as { timezone?: string } | null;
|
|
669
701
|
const conflictSlugs = ownerEmail
|
|
670
|
-
?
|
|
671
|
-
Array.from(new Set([slug, ...slugs])),
|
|
672
|
-
)
|
|
702
|
+
? Array.from(new Set([slug, ...ownerLinkSlugs]))
|
|
673
703
|
: slug
|
|
674
704
|
? [slug]
|
|
675
705
|
: [];
|
|
676
|
-
const eligibleHosts = await getEligibleHostAvailability(
|
|
677
|
-
ownerEmail,
|
|
678
|
-
hostEmails,
|
|
679
|
-
);
|
|
680
706
|
|
|
681
707
|
return {
|
|
682
708
|
effectiveConfig:
|
|
@@ -685,9 +711,7 @@ async function resolveAvailabilityContext({
|
|
|
685
711
|
? createDefaultAvailability(
|
|
686
712
|
ownerSettings?.timezone || "America/New_York",
|
|
687
713
|
)
|
|
688
|
-
:
|
|
689
|
-
? null
|
|
690
|
-
: config),
|
|
714
|
+
: config),
|
|
691
715
|
ownerEmail,
|
|
692
716
|
hostEmails,
|
|
693
717
|
eligibleHosts,
|
|
@@ -1387,6 +1411,20 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
|
|
|
1387
1411
|
}
|
|
1388
1412
|
|
|
1389
1413
|
// Check for conflicts + insert atomically in a transaction
|
|
1414
|
+
const parsedConferencing = parseBookingConferencingConfig(
|
|
1415
|
+
link?.conferencing,
|
|
1416
|
+
);
|
|
1417
|
+
if (parsedConferencing.status === "invalid") {
|
|
1418
|
+
setResponseStatus(event, 422);
|
|
1419
|
+
return {
|
|
1420
|
+
error: "Failed to create booking",
|
|
1421
|
+
code: "invalid_conferencing_config",
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
const conferencing =
|
|
1425
|
+
parsedConferencing.status === "valid"
|
|
1426
|
+
? parsedConferencing.config
|
|
1427
|
+
: undefined;
|
|
1390
1428
|
const db = getDb();
|
|
1391
1429
|
const insertResult = await db.transaction(async (tx) => {
|
|
1392
1430
|
if (viewer) {
|
|
@@ -1469,6 +1507,7 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
|
|
|
1469
1507
|
? JSON.stringify(fieldResponses)
|
|
1470
1508
|
: null,
|
|
1471
1509
|
cancelToken,
|
|
1510
|
+
zoomNeedsReview: conferencing?.type === "zoom",
|
|
1472
1511
|
status: "confirmed",
|
|
1473
1512
|
createdAt: now,
|
|
1474
1513
|
ownerEmail: hostEmail,
|
|
@@ -1486,17 +1525,12 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
|
|
|
1486
1525
|
return { error: "This time slot is no longer available" };
|
|
1487
1526
|
}
|
|
1488
1527
|
|
|
1489
|
-
// Resolve conferencing config
|
|
1490
|
-
let conferencing: ConferencingConfig | undefined;
|
|
1491
|
-
if (link?.conferencing) {
|
|
1492
|
-
try {
|
|
1493
|
-
conferencing = JSON.parse(link.conferencing);
|
|
1494
|
-
} catch {}
|
|
1495
|
-
}
|
|
1496
1528
|
let meetingLink: string | undefined;
|
|
1497
1529
|
let googleEventId: string | undefined;
|
|
1498
1530
|
let calendarAccountId: string | undefined;
|
|
1499
1531
|
let meetingLinkPending = false;
|
|
1532
|
+
let zoomMeetingId: string | undefined;
|
|
1533
|
+
let zoomAccountId: string | undefined;
|
|
1500
1534
|
|
|
1501
1535
|
// For custom-URL conferencing, use the static URL — only http(s).
|
|
1502
1536
|
if (conferencing?.type === "custom" && conferencing.url) {
|
|
@@ -1536,6 +1570,8 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
|
|
|
1536
1570
|
throw new Error("Zoom meeting was not created");
|
|
1537
1571
|
}
|
|
1538
1572
|
meetingLink = zoomResult.meetingUrl;
|
|
1573
|
+
zoomMeetingId = zoomResult.meetingId;
|
|
1574
|
+
zoomAccountId = zoomResult.accountId;
|
|
1539
1575
|
} catch (error) {
|
|
1540
1576
|
console.error(
|
|
1541
1577
|
`[bookings] Failed to create Zoom meeting for ${hostEmail}:`,
|
|
@@ -1543,8 +1579,7 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
|
|
|
1543
1579
|
);
|
|
1544
1580
|
// Zoom may have created the meeting even if its response was lost or
|
|
1545
1581
|
// unreadable, so keep the booking to reserve the slot and prevent a
|
|
1546
|
-
// retry from silently creating a duplicate meeting.
|
|
1547
|
-
// independent calendar write before returning the provider error.
|
|
1582
|
+
// retry from silently creating a duplicate meeting.
|
|
1548
1583
|
meetingLinkPending = true;
|
|
1549
1584
|
}
|
|
1550
1585
|
}
|
|
@@ -1634,10 +1669,23 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
|
|
|
1634
1669
|
meetingLink?: string;
|
|
1635
1670
|
googleEventId?: string;
|
|
1636
1671
|
calendarAccountId?: string;
|
|
1672
|
+
zoomNeedsReview?: boolean;
|
|
1673
|
+
zoomMeetingId?: string;
|
|
1674
|
+
zoomAccountId?: string;
|
|
1637
1675
|
meetingLinkPending: boolean;
|
|
1638
1676
|
} = { meetingLinkPending };
|
|
1639
1677
|
if (meetingLink) providerUpdates.meetingLink = meetingLink;
|
|
1640
1678
|
if (googleEventId) providerUpdates.googleEventId = googleEventId;
|
|
1679
|
+
if (zoomMeetingId) providerUpdates.zoomMeetingId = zoomMeetingId;
|
|
1680
|
+
if (zoomAccountId) providerUpdates.zoomAccountId = zoomAccountId;
|
|
1681
|
+
if (
|
|
1682
|
+
conferencing?.type === "zoom" &&
|
|
1683
|
+
meetingLink &&
|
|
1684
|
+
zoomMeetingId &&
|
|
1685
|
+
zoomAccountId
|
|
1686
|
+
) {
|
|
1687
|
+
providerUpdates.zoomNeedsReview = false;
|
|
1688
|
+
}
|
|
1641
1689
|
if (googleEventId && calendarAccountId) {
|
|
1642
1690
|
providerUpdates.calendarAccountId = calendarAccountId;
|
|
1643
1691
|
}
|
|
@@ -1664,6 +1712,7 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
|
|
|
1664
1712
|
...(meetingLinkPending ? { meetingLinkPending: true } : {}),
|
|
1665
1713
|
googleEventId,
|
|
1666
1714
|
cancelToken,
|
|
1715
|
+
zoomNeedsReview: conferencing?.type === "zoom" && meetingLinkPending,
|
|
1667
1716
|
status: "confirmed",
|
|
1668
1717
|
createdAt: now,
|
|
1669
1718
|
};
|
|
@@ -1897,6 +1946,7 @@ export const getAvailableSlots = defineEventHandler(async (event: H3Event) => {
|
|
|
1897
1946
|
export async function cancelBookingById(
|
|
1898
1947
|
id: string,
|
|
1899
1948
|
origin = getAppProductionUrl(),
|
|
1949
|
+
options: { zoomMeetingResolved?: boolean } = {},
|
|
1900
1950
|
) {
|
|
1901
1951
|
if (!id)
|
|
1902
1952
|
throw createError({ statusCode: 400, statusMessage: "id is required" });
|
|
@@ -1914,11 +1964,23 @@ export async function cancelBookingById(
|
|
|
1914
1964
|
|
|
1915
1965
|
// Bookings have no ownerEmail of their own — scope through the booking link.
|
|
1916
1966
|
const accessibleLinks = await db
|
|
1917
|
-
.select({
|
|
1967
|
+
.select({
|
|
1968
|
+
slug: schema.bookingLinks.slug,
|
|
1969
|
+
ownerEmail: schema.bookingLinks.ownerEmail,
|
|
1970
|
+
hosts: schema.bookingLinks.hosts,
|
|
1971
|
+
conferencing: schema.bookingLinks.conferencing,
|
|
1972
|
+
})
|
|
1918
1973
|
.from(schema.bookingLinks)
|
|
1919
|
-
.where(
|
|
1920
|
-
|
|
1921
|
-
|
|
1974
|
+
.where(
|
|
1975
|
+
accessFilter(
|
|
1976
|
+
schema.bookingLinks,
|
|
1977
|
+
schema.bookingLinkShares,
|
|
1978
|
+
undefined,
|
|
1979
|
+
"editor",
|
|
1980
|
+
),
|
|
1981
|
+
);
|
|
1982
|
+
const link = accessibleLinks.find((item) => item.slug === existing.slug);
|
|
1983
|
+
if (!link) {
|
|
1922
1984
|
throw createError({ statusCode: 403, statusMessage: "Access denied" });
|
|
1923
1985
|
}
|
|
1924
1986
|
|
|
@@ -1926,7 +1988,35 @@ export async function cancelBookingById(
|
|
|
1926
1988
|
return { success: true, alreadyCancelled: true };
|
|
1927
1989
|
}
|
|
1928
1990
|
|
|
1929
|
-
|
|
1991
|
+
if (
|
|
1992
|
+
options.zoomMeetingResolved === true &&
|
|
1993
|
+
!isBookingLinkHost(link, getRequestContext()?.userEmail)
|
|
1994
|
+
) {
|
|
1995
|
+
throw createError({ statusCode: 403, statusMessage: "Access denied" });
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
if (
|
|
1999
|
+
needsZoomCancellationReview({
|
|
2000
|
+
...existing,
|
|
2001
|
+
conferencing: link.conferencing,
|
|
2002
|
+
}) &&
|
|
2003
|
+
options.zoomMeetingResolved !== true
|
|
2004
|
+
) {
|
|
2005
|
+
throw createError({
|
|
2006
|
+
statusCode: 409,
|
|
2007
|
+
statusMessage:
|
|
2008
|
+
"Check the Zoom meeting and resolve it before canceling this booking",
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (existing.zoomMeetingId && existing.zoomAccountId) {
|
|
2013
|
+
await deleteZoomMeeting({
|
|
2014
|
+
accountId: existing.zoomAccountId,
|
|
2015
|
+
meetingId: existing.zoomMeetingId,
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const hostEmail = link.ownerEmail;
|
|
1930
2020
|
const bookingTimeZone = await getOwnerBookingTimeZone(hostEmail);
|
|
1931
2021
|
const bookAgainUrl = existing.slug
|
|
1932
2022
|
? `${origin}/book/${existing.slug}`
|
|
@@ -1940,7 +2030,7 @@ export async function cancelBookingById(
|
|
|
1940
2030
|
await deleteGoogleEventForBooking({ booking: existing, hostEmail });
|
|
1941
2031
|
await db
|
|
1942
2032
|
.update(schema.bookings)
|
|
1943
|
-
.set({ status: "cancelled" })
|
|
2033
|
+
.set({ status: "cancelled", zoomNeedsReview: false })
|
|
1944
2034
|
.where(eq(schema.bookings.id, id));
|
|
1945
2035
|
recordBookingsChanged(hostEmail);
|
|
1946
2036
|
return { success: true };
|
|
@@ -1949,9 +2039,19 @@ export async function cancelBookingById(
|
|
|
1949
2039
|
export const deleteBooking = defineEventHandler(async (event: H3Event) => {
|
|
1950
2040
|
return requireRequestContext(event, async () => {
|
|
1951
2041
|
try {
|
|
2042
|
+
const body = await readBody(event);
|
|
2043
|
+
const parsed = z
|
|
2044
|
+
.object({ zoomMeetingResolved: z.boolean().optional() })
|
|
2045
|
+
.strict()
|
|
2046
|
+
.safeParse(body ?? {});
|
|
2047
|
+
if (!parsed.success) {
|
|
2048
|
+
setResponseStatus(event, 400);
|
|
2049
|
+
return { error: "Invalid cancellation options" };
|
|
2050
|
+
}
|
|
1952
2051
|
return await cancelBookingById(
|
|
1953
2052
|
getRouterParam(event, "id") as string,
|
|
1954
2053
|
getRequestURL(event).origin,
|
|
2054
|
+
parsed.data,
|
|
1955
2055
|
);
|
|
1956
2056
|
} catch (error: any) {
|
|
1957
2057
|
setResponseStatus(event, error?.statusCode ?? 500);
|
|
@@ -1982,6 +2082,7 @@ export const getBookingByToken = defineEventHandler(async (event: H3Event) => {
|
|
|
1982
2082
|
|
|
1983
2083
|
// Return limited info — don't expose internal IDs
|
|
1984
2084
|
const booking = rowToBooking(row);
|
|
2085
|
+
const link = await getBookingLinkDetails(row.slug);
|
|
1985
2086
|
return {
|
|
1986
2087
|
eventTitle: booking.eventTitle,
|
|
1987
2088
|
name: booking.name,
|
|
@@ -1989,6 +2090,10 @@ export const getBookingByToken = defineEventHandler(async (event: H3Event) => {
|
|
|
1989
2090
|
end: booking.end,
|
|
1990
2091
|
slug: booking.slug,
|
|
1991
2092
|
meetingLink: booking.meetingLink,
|
|
2093
|
+
zoomCancellationNeedsReview: needsZoomCancellationReview({
|
|
2094
|
+
...row,
|
|
2095
|
+
conferencing: link?.conferencing,
|
|
2096
|
+
}),
|
|
1992
2097
|
meetingLinkPending: booking.meetingLinkPending,
|
|
1993
2098
|
status: booking.status,
|
|
1994
2099
|
};
|
|
@@ -2024,12 +2129,34 @@ export const cancelBookingByToken = defineEventHandler(
|
|
|
2024
2129
|
return { success: true, alreadyCancelled: true };
|
|
2025
2130
|
}
|
|
2026
2131
|
|
|
2132
|
+
const link = await getBookingLinkDetails(row.slug);
|
|
2133
|
+
if (
|
|
2134
|
+
needsZoomCancellationReview({
|
|
2135
|
+
...row,
|
|
2136
|
+
conferencing: link?.conferencing,
|
|
2137
|
+
})
|
|
2138
|
+
) {
|
|
2139
|
+
setResponseStatus(event, 409);
|
|
2140
|
+
return {
|
|
2141
|
+
error:
|
|
2142
|
+
"The organizer must review the Zoom meeting before this booking can be canceled",
|
|
2143
|
+
code: "zoom_meeting_review_required",
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
if (row.zoomMeetingId && row.zoomAccountId) {
|
|
2148
|
+
await deleteZoomMeeting({
|
|
2149
|
+
accountId: row.zoomAccountId,
|
|
2150
|
+
meetingId: row.zoomMeetingId,
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2027
2154
|
await db
|
|
2028
2155
|
.update(schema.bookings)
|
|
2029
|
-
.set({ status: "cancelled" })
|
|
2156
|
+
.set({ status: "cancelled", zoomNeedsReview: false })
|
|
2030
2157
|
.where(eq(schema.bookings.id, row.id));
|
|
2031
2158
|
|
|
2032
|
-
const hostEmail =
|
|
2159
|
+
const hostEmail = link?.ownerEmail;
|
|
2033
2160
|
const bookingTimeZone = await getOwnerBookingTimeZone(hostEmail);
|
|
2034
2161
|
const reqUrl = getRequestURL(event);
|
|
2035
2162
|
const bookAgainUrl = row.slug
|
|
@@ -2046,7 +2173,7 @@ export const cancelBookingByToken = defineEventHandler(
|
|
|
2046
2173
|
|
|
2047
2174
|
return { success: true, slug: row.slug };
|
|
2048
2175
|
} catch (error: any) {
|
|
2049
|
-
setResponseStatus(event, 500);
|
|
2176
|
+
setResponseStatus(event, error?.statusCode ?? 500);
|
|
2050
2177
|
return { error: error.message };
|
|
2051
2178
|
}
|
|
2052
2179
|
},
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BookingHost,
|
|
5
|
+
BookingLink,
|
|
6
|
+
ConferencingConfig,
|
|
7
|
+
} from "../../shared/api.js";
|
|
2
8
|
import { schema } from "../db/index.js";
|
|
3
9
|
|
|
4
10
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
11
|
+
const httpUrlSchema = z
|
|
12
|
+
.string()
|
|
13
|
+
.url()
|
|
14
|
+
.refine((value) => /^https?:\/\//i.test(value));
|
|
5
15
|
|
|
6
16
|
function stripCrlf(value: unknown): string {
|
|
7
17
|
return (
|
|
@@ -29,6 +39,33 @@ function parseJson<T>(value: string | null, fallback: T): T {
|
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
const conferencingConfigSchema = z.discriminatedUnion("type", [
|
|
43
|
+
z.object({ type: z.literal("none"), url: z.string().optional() }),
|
|
44
|
+
z.object({ type: z.literal("google_meet"), url: z.string().optional() }),
|
|
45
|
+
z.object({ type: z.literal("zoom"), url: z.string().optional() }),
|
|
46
|
+
z.object({
|
|
47
|
+
type: z.literal("custom"),
|
|
48
|
+
url: httpUrlSchema,
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export function parseBookingConferencingConfig(
|
|
53
|
+
value: string | null | undefined,
|
|
54
|
+
):
|
|
55
|
+
| { status: "absent" }
|
|
56
|
+
| { status: "invalid" }
|
|
57
|
+
| { status: "valid"; config: ConferencingConfig } {
|
|
58
|
+
if (value == null) return { status: "absent" };
|
|
59
|
+
try {
|
|
60
|
+
const parsed = conferencingConfigSchema.safeParse(JSON.parse(value));
|
|
61
|
+
return parsed.success
|
|
62
|
+
? { status: "valid", config: parsed.data }
|
|
63
|
+
: { status: "invalid" };
|
|
64
|
+
} catch {
|
|
65
|
+
return { status: "invalid" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
32
69
|
export function normalizeBookingHosts(
|
|
33
70
|
input: unknown,
|
|
34
71
|
ownerEmail?: string | null,
|
|
@@ -100,6 +137,14 @@ export function getBookingLinkRequiredHostEmails(
|
|
|
100
137
|
return emails;
|
|
101
138
|
}
|
|
102
139
|
|
|
140
|
+
export function isBookingLinkHost(
|
|
141
|
+
row: Pick<typeof schema.bookingLinks.$inferSelect, "hosts" | "ownerEmail">,
|
|
142
|
+
userEmail: unknown,
|
|
143
|
+
): boolean {
|
|
144
|
+
const email = normalizeBookingHostEmail(userEmail);
|
|
145
|
+
return !!email && getBookingLinkRequiredHostEmails(row).includes(email);
|
|
146
|
+
}
|
|
147
|
+
|
|
103
148
|
export function rowToBookingLink(
|
|
104
149
|
row: typeof schema.bookingLinks.$inferSelect,
|
|
105
150
|
): BookingLink {
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
*/
|
|
25
25
|
import { nanoid } from "nanoid";
|
|
26
26
|
|
|
27
|
+
import { parseBookingConferencingConfig } from "./booking-link-utils.js";
|
|
28
|
+
|
|
27
29
|
const PROVIDER = "zoom_video";
|
|
28
30
|
const SCOPES = [
|
|
29
31
|
"meeting:write:meeting",
|
|
@@ -56,6 +58,51 @@ export function getZoomAuthUrl(redirectUri: string, state: string) {
|
|
|
56
58
|
return `https://zoom.us/oauth/authorize?${params}`;
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
function createProvider(creds: { clientId: string; clientSecret: string }) {
|
|
62
|
+
return createZoomProvider({
|
|
63
|
+
clientId: creds.clientId,
|
|
64
|
+
clientSecret: creds.clientSecret,
|
|
65
|
+
getAccessToken: (credentialId) => resolveAccessToken(credentialId),
|
|
66
|
+
updateTokens: async (credentialId, tokens) => {
|
|
67
|
+
const existing = await getOAuthTokens(PROVIDER, credentialId);
|
|
68
|
+
await saveOAuthTokens(PROVIDER, credentialId, {
|
|
69
|
+
...existing,
|
|
70
|
+
accessToken: tokens.accessToken,
|
|
71
|
+
refreshToken: tokens.refreshToken ?? (existing as any)?.refreshToken,
|
|
72
|
+
expiresAt: tokens.expiresAt?.getTime(),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function needsZoomCancellationReview(booking: {
|
|
79
|
+
zoomNeedsReview?: boolean;
|
|
80
|
+
meetingLink?: string | null;
|
|
81
|
+
zoomMeetingId?: string | null;
|
|
82
|
+
zoomAccountId?: string | null;
|
|
83
|
+
conferencing?: string | null;
|
|
84
|
+
status?: string;
|
|
85
|
+
}): boolean {
|
|
86
|
+
if (booking.status === "cancelled") return false;
|
|
87
|
+
if (booking.zoomMeetingId && booking.zoomAccountId) return false;
|
|
88
|
+
if (booking.zoomNeedsReview) return true;
|
|
89
|
+
const conferencing = parseBookingConferencingConfig(booking.conferencing);
|
|
90
|
+
if (conferencing.status === "invalid") return true;
|
|
91
|
+
if (conferencing.status === "valid" && conferencing.config.type === "zoom") {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (!booking.meetingLink) return false;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const hostname = new URL(booking.meetingLink).hostname.toLowerCase();
|
|
98
|
+
return ["zoom.us", "zoom.com", "zoomgov.com"].some(
|
|
99
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`),
|
|
100
|
+
);
|
|
101
|
+
} catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
59
106
|
/**
|
|
60
107
|
* Exchange an authorization code for Zoom tokens. Stores them in
|
|
61
108
|
* `oauth_tokens(provider="zoom_video", account_id=<zoom_user_id>, owner=<ownerEmail>)`.
|
|
@@ -168,7 +215,12 @@ export async function disconnectZoom(ownerEmail: string) {
|
|
|
168
215
|
* owned by the host. `not_started` means the meeting creation request was not sent.
|
|
169
216
|
*/
|
|
170
217
|
export type ZoomMeetingResult =
|
|
171
|
-
| {
|
|
218
|
+
| {
|
|
219
|
+
status: "created";
|
|
220
|
+
meetingUrl: string;
|
|
221
|
+
meetingId: string;
|
|
222
|
+
accountId: string;
|
|
223
|
+
}
|
|
172
224
|
| { status: "not_started" }
|
|
173
225
|
| { status: "rejected" };
|
|
174
226
|
|
|
@@ -248,9 +300,22 @@ export async function createZoomMeeting(opts: {
|
|
|
248
300
|
status: "created",
|
|
249
301
|
meetingUrl: result.meetingUrl,
|
|
250
302
|
meetingId: result.meetingId,
|
|
303
|
+
accountId: credentialId,
|
|
251
304
|
};
|
|
252
305
|
}
|
|
253
306
|
|
|
307
|
+
export async function deleteZoomMeeting(opts: {
|
|
308
|
+
accountId: string;
|
|
309
|
+
meetingId: string;
|
|
310
|
+
}): Promise<void> {
|
|
311
|
+
const creds = getZoomCreds();
|
|
312
|
+
if (!creds) throw new Error("Zoom OAuth is not configured");
|
|
313
|
+
await createProvider(creds).deleteMeeting!({
|
|
314
|
+
credentialId: opts.accountId,
|
|
315
|
+
meetingId: opts.meetingId,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
254
319
|
/**
|
|
255
320
|
* Resolve a fresh access token for a Zoom credential, refreshing it if it's
|
|
256
321
|
* expired (or near-expiry). Persists the refreshed tokens back to oauth_tokens.
|