@agent-native/scheduling 0.1.23 → 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/README.md +16 -0
- package/agent-native.package.json +154 -0
- 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/dist/manifest.d.ts +5 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +6 -1
- package/dist/manifest.js.map +1 -1
- package/dist/server/availability-engine.d.ts +20 -0
- package/dist/server/availability-engine.d.ts.map +1 -1
- package/dist/server/availability-engine.js +73 -23
- package/dist/server/availability-engine.js.map +1 -1
- package/dist/server/booking-service.d.ts.map +1 -1
- package/dist/server/booking-service.js +16 -5
- package/dist/server/booking-service.js.map +1 -1
- package/dist/server/bookings-repo.d.ts.map +1 -1
- package/dist/server/bookings-repo.js +50 -48
- package/dist/server/bookings-repo.js.map +1 -1
- package/dist/server/providers/index.d.ts +2 -0
- package/dist/server/providers/index.d.ts.map +1 -1
- package/dist/server/providers/index.js +1 -0
- package/dist/server/providers/index.js.map +1 -1
- package/dist/server/providers/teams.d.ts +22 -0
- package/dist/server/providers/teams.d.ts.map +1 -0
- package/dist/server/providers/teams.js +112 -0
- package/dist/server/providers/teams.js.map +1 -0
- package/docs/eject.md +11 -10
- package/docs/llms-full.txt +75 -14
- package/docs/llms.txt +15 -1
- package/docs/providers.md +30 -2
- package/docs/skills/integrations/SKILL.md +19 -1
- package/package.json +7 -6
- 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
- package/src/manifest.test.ts +19 -0
- package/src/manifest.ts +9 -7
- package/src/server/availability-engine.test.ts +344 -0
- package/src/server/availability-engine.ts +100 -22
- package/src/server/booking-service.spec.ts +557 -0
- package/src/server/booking-service.ts +17 -6
- package/src/server/bookings-repo.ts +54 -48
- package/src/server/providers/index.ts +2 -0
- package/src/server/providers/teams.test.ts +246 -0
- package/src/server/providers/teams.ts +183 -0
package/docs/llms-full.txt
CHANGED
|
@@ -162,19 +162,20 @@ owned by or shared with the current user, read/write actions throw.
|
|
|
162
162
|
|
|
163
163
|
For full customization, you can move the package source into your own repo.
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
Preview the complete change report, then apply it explicitly:
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
```bash
|
|
168
|
+
agent-native package eject @agent-native/scheduling
|
|
169
|
+
agent-native package eject @agent-native/scheduling --apply
|
|
170
|
+
```
|
|
168
171
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
`@local/scheduling`.
|
|
175
|
-
5. Install: `pnpm install`.
|
|
172
|
+
The command copies the published source into `packages/scheduling`, updates the
|
|
173
|
+
consumer dependency to `workspace:*`, preserves canonical
|
|
174
|
+
`@agent-native/scheduling` imports, and runs the detected package manager. It
|
|
175
|
+
refuses an existing target, unsupported workspace layout, or other collision.
|
|
176
|
+
All writes and lockfiles roll back if installation fails.
|
|
176
177
|
|
|
177
|
-
Now you own the code and can modify freely. Upstream updates are available via
|
|
178
|
+
Now you own the code and can modify it freely. Upstream updates are available via
|
|
178
179
|
`npm view @agent-native/scheduling versions` — you can selectively port changes.
|
|
179
180
|
|
|
180
181
|
|
|
@@ -200,7 +201,7 @@ locally-owned scheduling stack, not an API wrapper.
|
|
|
200
201
|
application, booking-limit bucketing, recurring-event expansion.
|
|
201
202
|
- **Server layer** (`/server`) — DB repos, availability engine, booking
|
|
202
203
|
service, pluggable calendar + video providers (Google Calendar, Outlook,
|
|
203
|
-
Zoom, Google Meet, built-in video), iCal generator, lifecycle hooks →
|
|
204
|
+
Zoom, Microsoft Teams, Google Meet, built-in video), iCal generator, lifecycle hooks →
|
|
204
205
|
workflows.
|
|
205
206
|
- **Actions** (`/actions/*`) — ~55 `defineAction` modules covering every
|
|
206
207
|
operation in the product (event type CRUD, availability edit, booking
|
|
@@ -228,6 +229,7 @@ import {
|
|
|
228
229
|
registerVideoProvider,
|
|
229
230
|
createGoogleCalendarProvider,
|
|
230
231
|
createDailyVideoProvider,
|
|
232
|
+
createTeamsProvider,
|
|
231
233
|
} from "@agent-native/scheduling/server/providers";
|
|
232
234
|
import { getDb, schema } from "./db/index.js";
|
|
233
235
|
|
|
@@ -248,6 +250,12 @@ registerCalendarProvider(createGoogleCalendarProvider({
|
|
|
248
250
|
registerVideoProvider(createDailyVideoProvider({
|
|
249
251
|
apiKey: process.env.DAILY_API_KEY!,
|
|
250
252
|
}));
|
|
253
|
+
|
|
254
|
+
registerVideoProvider(createTeamsProvider({
|
|
255
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
256
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
257
|
+
getAccessToken: fetchMicrosoftToken, // your refresh-aware implementation
|
|
258
|
+
}));
|
|
251
259
|
```
|
|
252
260
|
|
|
253
261
|
## Full documentation
|
|
@@ -255,6 +263,13 @@ registerVideoProvider(createDailyVideoProvider({
|
|
|
255
263
|
For the full bundled docs (every .md file concatenated), see
|
|
256
264
|
`llms-full.txt` in this folder.
|
|
257
265
|
|
|
266
|
+
## Package lifecycle
|
|
267
|
+
|
|
268
|
+
The published static `agent-native.package.json` drives safe package lifecycle
|
|
269
|
+
commands. Use `agent-native package inspect @agent-native/scheduling --json`
|
|
270
|
+
for contribution metadata, and preview `package add` or `package eject` before
|
|
271
|
+
re-running with `--apply`. Inspection never executes package code.
|
|
272
|
+
|
|
258
273
|
## Individual skills
|
|
259
274
|
|
|
260
275
|
- skills/scheduling-basics/SKILL.md — Core concepts, terminology.
|
|
@@ -282,8 +297,8 @@ Scheduling needs two kinds of providers:
|
|
|
282
297
|
- **CalendarProvider** — reads busy intervals + writes events.
|
|
283
298
|
- **VideoProvider** — creates meeting rooms when a booking is confirmed.
|
|
284
299
|
|
|
285
|
-
Built-ins: Google Calendar, Office 365, Zoom, built-in video
|
|
286
|
-
Meet (piggy-backs on Google Calendar).
|
|
300
|
+
Built-ins: Google Calendar, Office 365, Zoom, Microsoft Teams, built-in video
|
|
301
|
+
(Daily.co), and Google Meet (piggy-backs on Google Calendar).
|
|
287
302
|
|
|
288
303
|
## CalendarProvider
|
|
289
304
|
|
|
@@ -344,6 +359,34 @@ Video providers are invoked by the booking service when a booking's location
|
|
|
344
359
|
is `builtin-video`, `zoom`, `google-meet`, or `teams`. Meeting URLs land in
|
|
345
360
|
`booking_references`.
|
|
346
361
|
|
|
362
|
+
### Microsoft Teams
|
|
363
|
+
|
|
364
|
+
Teams uses delegated Microsoft OAuth for work or school accounts. Register the
|
|
365
|
+
provider with consumer-owned token storage and refresh callbacks:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import {
|
|
369
|
+
createTeamsProvider,
|
|
370
|
+
registerVideoProvider,
|
|
371
|
+
} from "@agent-native/scheduling/server/providers";
|
|
372
|
+
|
|
373
|
+
registerVideoProvider(
|
|
374
|
+
createTeamsProvider({
|
|
375
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
376
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
377
|
+
tenant: process.env.MICROSOFT_TENANT_ID,
|
|
378
|
+
getAccessToken: resolveMicrosoftAccessToken,
|
|
379
|
+
updateTokens: saveMicrosoftTokens,
|
|
380
|
+
markInvalid: markSchedulingCredentialInvalid,
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The provider requests `offline_access`, `OnlineMeetings.ReadWrite`, and
|
|
386
|
+
`User.Read`. Start the grant with `connect-video --kind teams_video` and finish
|
|
387
|
+
the callback with `completeVideoOAuth()`. `getAccessToken` must refresh expired
|
|
388
|
+
tokens; the package never reads a token store or environment directly.
|
|
389
|
+
|
|
347
390
|
|
|
348
391
|
---
|
|
349
392
|
## schema.md
|
|
@@ -710,7 +753,8 @@ description: Calendar + video provider integrations — Google Calendar, Office
|
|
|
710
753
|
- **builtin_video** — Daily.co-backed; zero OAuth; server-to-server API key.
|
|
711
754
|
- **zoom_video** — OAuth; create meetings via Zoom REST API.
|
|
712
755
|
- **google_meet** — piggy-backs on Google Calendar credential.
|
|
713
|
-
- **teams_video**
|
|
756
|
+
- **teams_video** — delegated Microsoft OAuth for work or school accounts;
|
|
757
|
+
creates and cancels standalone meetings through Microsoft Graph.
|
|
714
758
|
|
|
715
759
|
## Credential lifecycle
|
|
716
760
|
|
|
@@ -742,8 +786,25 @@ See `docs/providers.md` for the full interface.
|
|
|
742
786
|
| "Connect Google Calendar" | `connect-calendar --kind google_calendar --redirectUri ...` → redirect to returned `authUrl` |
|
|
743
787
|
| "Stop checking my vacation calendar" | `toggle-selected-calendar --include false` for that externalId |
|
|
744
788
|
| "Default to Zoom for new bookings" | `set-default-conferencing-app --credentialId <zoom-cred>` |
|
|
789
|
+
| "Connect Microsoft Teams" | `connect-video --kind teams_video --redirectUri ...` → complete the callback with `completeVideoOAuth()` |
|
|
790
|
+
| "Default to Teams for new bookings" | `set-default-conferencing-app --credentialId <teams-cred>` |
|
|
745
791
|
| "Refresh calendar cache" | `refresh-busy-times` |
|
|
746
792
|
|
|
793
|
+
## Microsoft Teams setup
|
|
794
|
+
|
|
795
|
+
Register `createTeamsProvider()` as a video provider with the deployment's
|
|
796
|
+
Microsoft client id and secret plus consumer-owned `getAccessToken`,
|
|
797
|
+
`updateTokens`, and `markInvalid` callbacks. The provider defaults to the
|
|
798
|
+
`organizations` tenant because Graph's delegated online-meeting API supports
|
|
799
|
+
work or school accounts, not personal Microsoft accounts. It requests only
|
|
800
|
+
`offline_access`, `OnlineMeetings.ReadWrite`, and `User.Read`.
|
|
801
|
+
|
|
802
|
+
Use `connect-video` to start OAuth and `completeVideoOAuth()` in the callback;
|
|
803
|
+
inserting a `teams_video` row with `install-conferencing-app` alone does not
|
|
804
|
+
grant Microsoft access. Token refresh remains the consumer's responsibility in
|
|
805
|
+
`getAccessToken`. The booking service stores the returned meeting id and join
|
|
806
|
+
URL in `booking_references` and calls the provider again on cancellation.
|
|
807
|
+
|
|
747
808
|
|
|
748
809
|
---
|
|
749
810
|
## skills/routing-forms/SKILL.md
|
package/docs/llms.txt
CHANGED
|
@@ -17,7 +17,7 @@ locally-owned scheduling stack, not an API wrapper.
|
|
|
17
17
|
application, booking-limit bucketing, recurring-event expansion.
|
|
18
18
|
- **Server layer** (`/server`) — DB repos, availability engine, booking
|
|
19
19
|
service, pluggable calendar + video providers (Google Calendar, Outlook,
|
|
20
|
-
Zoom, Google Meet, built-in video), iCal generator, lifecycle hooks →
|
|
20
|
+
Zoom, Microsoft Teams, Google Meet, built-in video), iCal generator, lifecycle hooks →
|
|
21
21
|
workflows.
|
|
22
22
|
- **Actions** (`/actions/*`) — ~55 `defineAction` modules covering every
|
|
23
23
|
operation in the product (event type CRUD, availability edit, booking
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
registerVideoProvider,
|
|
46
46
|
createGoogleCalendarProvider,
|
|
47
47
|
createDailyVideoProvider,
|
|
48
|
+
createTeamsProvider,
|
|
48
49
|
} from "@agent-native/scheduling/server/providers";
|
|
49
50
|
import { getDb, schema } from "./db/index.js";
|
|
50
51
|
|
|
@@ -65,6 +66,12 @@ registerCalendarProvider(createGoogleCalendarProvider({
|
|
|
65
66
|
registerVideoProvider(createDailyVideoProvider({
|
|
66
67
|
apiKey: process.env.DAILY_API_KEY!,
|
|
67
68
|
}));
|
|
69
|
+
|
|
70
|
+
registerVideoProvider(createTeamsProvider({
|
|
71
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
72
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
73
|
+
getAccessToken: fetchMicrosoftToken, // your refresh-aware implementation
|
|
74
|
+
}));
|
|
68
75
|
```
|
|
69
76
|
|
|
70
77
|
## Full documentation
|
|
@@ -72,6 +79,13 @@ registerVideoProvider(createDailyVideoProvider({
|
|
|
72
79
|
For the full bundled docs (every .md file concatenated), see
|
|
73
80
|
`llms-full.txt` in this folder.
|
|
74
81
|
|
|
82
|
+
## Package lifecycle
|
|
83
|
+
|
|
84
|
+
The published static `agent-native.package.json` drives safe package lifecycle
|
|
85
|
+
commands. Use `agent-native package inspect @agent-native/scheduling --json`
|
|
86
|
+
for contribution metadata, and preview `package add` or `package eject` before
|
|
87
|
+
re-running with `--apply`. Inspection never executes package code.
|
|
88
|
+
|
|
75
89
|
## Individual skills
|
|
76
90
|
|
|
77
91
|
- skills/scheduling-basics/SKILL.md — Core concepts, terminology.
|
package/docs/providers.md
CHANGED
|
@@ -5,8 +5,8 @@ Scheduling needs two kinds of providers:
|
|
|
5
5
|
- **CalendarProvider** — reads busy intervals + writes events.
|
|
6
6
|
- **VideoProvider** — creates meeting rooms when a booking is confirmed.
|
|
7
7
|
|
|
8
|
-
Built-ins: Google Calendar, Office 365, Zoom, built-in video
|
|
9
|
-
Meet (piggy-backs on Google Calendar).
|
|
8
|
+
Built-ins: Google Calendar, Office 365, Zoom, Microsoft Teams, built-in video
|
|
9
|
+
(Daily.co), and Google Meet (piggy-backs on Google Calendar).
|
|
10
10
|
|
|
11
11
|
## CalendarProvider
|
|
12
12
|
|
|
@@ -66,3 +66,31 @@ registerVideoProvider({
|
|
|
66
66
|
Video providers are invoked by the booking service when a booking's location
|
|
67
67
|
is `builtin-video`, `zoom`, `google-meet`, or `teams`. Meeting URLs land in
|
|
68
68
|
`booking_references`.
|
|
69
|
+
|
|
70
|
+
### Microsoft Teams
|
|
71
|
+
|
|
72
|
+
Teams uses delegated Microsoft OAuth for work or school accounts. Register the
|
|
73
|
+
provider with consumer-owned token storage and refresh callbacks:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {
|
|
77
|
+
createTeamsProvider,
|
|
78
|
+
registerVideoProvider,
|
|
79
|
+
} from "@agent-native/scheduling/server/providers";
|
|
80
|
+
|
|
81
|
+
registerVideoProvider(
|
|
82
|
+
createTeamsProvider({
|
|
83
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
84
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
|
|
85
|
+
tenant: process.env.MICROSOFT_TENANT_ID,
|
|
86
|
+
getAccessToken: resolveMicrosoftAccessToken,
|
|
87
|
+
updateTokens: saveMicrosoftTokens,
|
|
88
|
+
markInvalid: markSchedulingCredentialInvalid,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The provider requests `offline_access`, `OnlineMeetings.ReadWrite`, and
|
|
94
|
+
`User.Read`. Start the grant with `connect-video --kind teams_video` and finish
|
|
95
|
+
the callback with `completeVideoOAuth()`. `getAccessToken` must refresh expired
|
|
96
|
+
tokens; the package never reads a token store or environment directly.
|
|
@@ -17,7 +17,8 @@ description: Calendar + video provider integrations — Google Calendar, Office
|
|
|
17
17
|
- **builtin_video** — Daily.co-backed; zero OAuth; server-to-server API key.
|
|
18
18
|
- **zoom_video** — OAuth; create meetings via Zoom REST API.
|
|
19
19
|
- **google_meet** — piggy-backs on Google Calendar credential.
|
|
20
|
-
- **teams_video**
|
|
20
|
+
- **teams_video** — delegated Microsoft OAuth for work or school accounts;
|
|
21
|
+
creates and cancels standalone meetings through Microsoft Graph.
|
|
21
22
|
|
|
22
23
|
## Credential lifecycle
|
|
23
24
|
|
|
@@ -49,4 +50,21 @@ See `docs/providers.md` for the full interface.
|
|
|
49
50
|
| "Connect Google Calendar" | `connect-calendar --kind google_calendar --redirectUri ...` → redirect to returned `authUrl` |
|
|
50
51
|
| "Stop checking my vacation calendar" | `toggle-selected-calendar --include false` for that externalId |
|
|
51
52
|
| "Default to Zoom for new bookings" | `set-default-conferencing-app --credentialId <zoom-cred>` |
|
|
53
|
+
| "Connect Microsoft Teams" | `connect-video --kind teams_video --redirectUri ...` → complete the callback with `completeVideoOAuth()` |
|
|
54
|
+
| "Default to Teams for new bookings" | `set-default-conferencing-app --credentialId <teams-cred>` |
|
|
52
55
|
| "Refresh calendar cache" | `refresh-busy-times` |
|
|
56
|
+
|
|
57
|
+
## Microsoft Teams setup
|
|
58
|
+
|
|
59
|
+
Register `createTeamsProvider()` as a video provider with the deployment's
|
|
60
|
+
Microsoft client id and secret plus consumer-owned `getAccessToken`,
|
|
61
|
+
`updateTokens`, and `markInvalid` callbacks. The provider defaults to the
|
|
62
|
+
`organizations` tenant because Graph's delegated online-meeting API supports
|
|
63
|
+
work or school accounts, not personal Microsoft accounts. It requests only
|
|
64
|
+
`offline_access`, `OnlineMeetings.ReadWrite`, and `User.Read`.
|
|
65
|
+
|
|
66
|
+
Use `connect-video` to start OAuth and `completeVideoOAuth()` in the callback;
|
|
67
|
+
inserting a `teams_video` row with `install-conferencing-app` alone does not
|
|
68
|
+
grant Microsoft access. Token refresh remains the consumer's responsibility in
|
|
69
|
+
`getAccessToken`. The booking service stores the returned meeting id and join
|
|
70
|
+
URL in `booking_references` and calls the provider again on cancellation.
|
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": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"directory": "packages/scheduling"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
+
"agent-native.package.json",
|
|
16
17
|
"dist",
|
|
17
18
|
"docs",
|
|
18
19
|
"src"
|
|
@@ -52,23 +53,22 @@
|
|
|
52
53
|
"nanoid": "^5.1.9",
|
|
53
54
|
"rrule": "^2.8.1",
|
|
54
55
|
"zod": "^4.3.6",
|
|
55
|
-
"@agent-native/toolkit": "^0.4.
|
|
56
|
+
"@agent-native/toolkit": "^0.4.7"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@libsql/client": "^0.15.8",
|
|
59
|
-
"@tabler/icons-react": "^3.
|
|
60
|
+
"@tabler/icons-react": "^3.41.1",
|
|
60
61
|
"@types/node": "^24.2.1",
|
|
61
62
|
"@types/react": "^19.2.14",
|
|
62
63
|
"drizzle-orm": "^0.45.2",
|
|
63
64
|
"react": "^19.2.5",
|
|
64
|
-
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
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",
|
|
71
|
-
"@tabler/icons-react": "
|
|
71
|
+
"@tabler/icons-react": "^3.41.1",
|
|
72
72
|
"drizzle-orm": ">=0.45",
|
|
73
73
|
"react": ">=18",
|
|
74
74
|
"react-dom": ">=18"
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"optional": true
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
+
"agentNativeManifest": "agent-native.package.json",
|
|
87
88
|
"scripts": {
|
|
88
89
|
"build": "tsc && node scripts/build-llms-full.mjs",
|
|
89
90
|
"dev": "tsc --watch",
|
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
|
+
});
|