slotlock 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/NOTICE +2 -0
  3. package/README.md +4 -2
  4. package/agent-server.d.ts +25 -5
  5. package/agent-server.js +152 -7
  6. package/agent-store-backend.d.ts +6 -1
  7. package/agent-store-backend.js +1068 -68
  8. package/application-api.d.ts +28 -1
  9. package/application-api.js +789 -21
  10. package/approvals.d.ts +2 -0
  11. package/approvals.js +12 -5
  12. package/assistant.d.ts +34 -8
  13. package/assistant.js +81 -26
  14. package/availability-store.d.ts +193 -0
  15. package/availability-store.js +566 -0
  16. package/booking-rules.d.ts +103 -5
  17. package/booking-rules.js +263 -15
  18. package/config.d.ts +50 -0
  19. package/config.js +107 -0
  20. package/dashboard-assistant-ui.d.ts +1 -1
  21. package/dashboard-assistant-ui.js +109 -8
  22. package/dashboard-scope.d.ts +7 -0
  23. package/dashboard-scope.js +21 -0
  24. package/dashboard.d.ts +152 -8
  25. package/dashboard.js +3868 -399
  26. package/ddl-identity.d.ts +25 -0
  27. package/ddl-identity.js +881 -0
  28. package/ddl-scim.d.ts +5 -0
  29. package/ddl-scim.js +740 -0
  30. package/ddl-sso.d.ts +15 -0
  31. package/ddl-sso.js +1033 -0
  32. package/ddl.d.ts +44 -9
  33. package/ddl.js +2872 -47
  34. package/domains.d.ts +117 -0
  35. package/domains.js +340 -0
  36. package/event-feed.d.ts +2 -2
  37. package/event-feed.js +24 -34
  38. package/holiday-import.d.ts +38 -0
  39. package/holiday-import.js +56 -0
  40. package/holidays.d.ts +38 -0
  41. package/holidays.js +209 -0
  42. package/hours.d.ts +125 -0
  43. package/hours.js +224 -0
  44. package/index.d.ts +24 -5
  45. package/index.js +17 -3
  46. package/interop.d.ts +25 -7
  47. package/interop.js +145 -17
  48. package/invitation-email.d.ts +26 -0
  49. package/invitation-email.js +73 -0
  50. package/lifecycle.d.ts +31 -0
  51. package/lifecycle.js +115 -0
  52. package/maintenance.d.ts +26 -2
  53. package/maintenance.js +70 -2
  54. package/metrics.js +3 -0
  55. package/oidc.d.ts +135 -0
  56. package/oidc.js +452 -0
  57. package/openapi.json +20172 -9850
  58. package/package.json +2 -1
  59. package/ranges.d.ts +28 -0
  60. package/ranges.js +135 -0
  61. package/resource-search.d.ts +19 -0
  62. package/resource-search.js +153 -0
  63. package/rest-api.js +7 -0
  64. package/rules.d.ts +2 -0
  65. package/rules.js +2 -0
  66. package/scim-server.d.ts +19 -0
  67. package/scim-server.js +243 -0
  68. package/scim.d.ts +180 -0
  69. package/scim.js +416 -0
  70. package/self-host.d.ts +19 -6
  71. package/self-host.js +483 -28
  72. package/slotlock.manifest.json +1 -1
  73. package/store.d.ts +637 -23
  74. package/store.js +2529 -147
  75. package/types.d.ts +230 -0
  76. package/types.js +68 -1
  77. package/workspace-domains.d.ts +146 -0
  78. package/workspace-domains.js +249 -0
  79. package/workspace-roles.d.ts +47 -0
  80. package/workspace-roles.js +304 -0
  81. package/workspace-scim.d.ts +89 -0
  82. package/workspace-scim.js +157 -0
  83. package/workspace-sso.d.ts +131 -0
  84. package/workspace-sso.js +180 -0
  85. package/workspaces.d.ts +240 -0
  86. package/workspaces.js +433 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,548 @@
3
3
  All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
4
4
  versioning.
5
5
 
6
+ ## 0.10.0 - 2026-11-11
7
+
8
+ This release gives a workspace its people and an audit trail. It brings six roles read from the
9
+ database on every request, team workspaces with invitations, verified domains that let colleagues
10
+ find and ask to join their team, single sign-on and SCIM provisioning through each team's own
11
+ identity provider, sign-out everywhere and a hash-chained audit log, and lets a
12
+ member be limited to locations or groups (RFC 0004 D1). The change history
13
+ becomes append-only and is kept by plan (D2). A person's data can be exported or erased, and a whole
14
+ tenant exported (D3), from the API, the command line and the dashboard's new Privacy page. It closes
15
+ the four questions the security review left open, and fixes what a review of RFC 0004's T2 changes
16
+ found; that review was not the independent human review GOVERNANCE.md requires. Read the upgrade
17
+ notes first.
18
+
19
+ ### Upgrade notes
20
+
21
+ - **Run `slotlock migrate` before serving this release, on every server at once.**
22
+ - The schema becomes version 5, which a 0.9 server refuses (`schema_newer`), so a server still on
23
+ 0.9 stops serving once the database is migrated.
24
+ - It adds eight tables (`audit_events`, `dashboard_people`, `dashboard_sessions`, `workspaces`,
25
+ `workspace_memberships`, `workspace_invitations`, `tenant_exports`, `tenant_export_parts`), new
26
+ columns (among them `cutoff_anchor` on bookings and events, and the audit log's chain) and 39
27
+ functions.
28
+ - Verified domains add three more tables (`workspace_domains`, `workspace_join_requests`,
29
+ `workspace_join_blocks`) and single sign-on five (`sso_connections`, `sso_identities`,
30
+ `sso_role_mappings`, `dashboard_session_sso`, `sso_bypasses`) and SCIM four (`scim_tokens`,
31
+ `scim_users`, `scim_groups`, `scim_group_members`), with their functions, unused until turned on.
32
+ - It grants the serving role what this release needs; a library user runs
33
+ `grantApplicationRole` again.
34
+ - **The change history can no longer be edited by the serving role.** It may only add to
35
+ `slotlock.outbox` and read it. Maintenance prunes it through `slotlock.prune_changes`, so code of
36
+ your own that deleted or changed change rows directly must stop.
37
+ - **Everyone signs in again once.** Sessions from before this release carry no session epoch and no
38
+ session record, so the dashboard asks for a new sign-in.
39
+ - **An organization's members become Developers.** They keep read-only and read-write keys of
40
+ their own, but no longer create Full-access keys, add webhooks or revoke others' keys. Keys they
41
+ made before keep working: review the Full-access keys your organization's members created.
42
+ - **Set each resource's hold price before upgrading if its agent holds take payment.** An agent's
43
+ hold now takes its price from the resource's `holdPrice` booking rule (`hold_price`).
44
+ - An agent that names a `payment` the rule does not have is refused `payment_price_mismatch`.
45
+ That includes any payment on a resource without a price.
46
+ - An agent books a priced resource only from a paid hold (`payment_required`).
47
+ - **A key's caps count its pending approvals.** An agent close to its `max_holds` or
48
+ `max_held_hours` may be refused sooner (`hold_limit_reached`, `held_time_limit_reached`).
49
+ - **`@slotlock/sync`:** connect calendars connected before this release again (their seals are
50
+ now bound to their tenant). A library user who reads iCalendar URLs must pass the guarded fetch.
51
+ - **`@slotlock/payments-stripe` with ISK or UGX:** create PaymentIntents for
52
+ `stripeAmount(amountMinor, currency)`. A payment read back is converted with `isoMinorUnits`, and
53
+ one that is not a whole ISO amount is refused.
54
+ - **New and off until set:**
55
+ - team workspaces (`SLOTLOCK_DASHBOARD_TEAM_WORKSPACES=on`);
56
+ - invitations by email (`SLOTLOCK_INVITATION_EMAIL`, `SLOTLOCK_INVITATION_EMAIL_KEY`,
57
+ `SLOTLOCK_INVITATION_EMAIL_FROM`);
58
+ - verified domains, with team workspaces (`SLOTLOCK_VERIFIED_DOMAINS=on`; the server's maintenance
59
+ checks each domain daily, so set it on the server too);
60
+ - single sign-on through each team's provider, with verified domains (`SLOTLOCK_TEAM_SSO=on` and
61
+ `SLOTLOCK_SSO_SECRET`, on whichever serves the dashboard: a server that does not never reads
62
+ them);
63
+ - SCIM provisioning from each team's provider, with verified domains (`SLOTLOCK_TEAM_SCIM=on`, on
64
+ whichever serves the dashboard: its `/scim/v2` is where providers call). On Workers, add the
65
+ `SLOTLOCK_SCIM_RATE_LIMIT` rate limiting binding, which counts providers' calls apart from each
66
+ client's dashboard budget; without it they share that budget, and the Worker logs a warning.
67
+
68
+ ### Added
69
+
70
+ - **slotlock.pylota.io, for search engines** ([RFC 0005](docs/rfc/0005-search.md)). A page per
71
+ search intent: `/pricing/`, `/mcp-server/`, `/booking-api/`, `/open-source/`, `/about/`,
72
+ `/use-cases/` with eight industries, `/compare/` (a bookings table of your own, Cal.com, Cronofy,
73
+ Nylas and the Google Calendar API, each claim sourced and dated) and `/blog/` with four articles
74
+ and an RSS feed, each with a Markdown twin listed in `llms.txt`. Every page carries a search
75
+ title and description, an Open Graph image and JSON-LD (Organization, SoftwareApplication with
76
+ the plans' prices, BreadcrumbList, TechArticle, BlogPosting); the sitemap dates each page from
77
+ git; Markdown twins name their HTML page as canonical and agent files are `noindex`; the 404 page
78
+ is `noindex`. `npm run check:seo` fails the site build on a missing canonical, image or sitemap
79
+ entry, and `scripts/check-live-seo.sh` checks the live hosts after each deploy. The site allows
80
+ Cloudflare Web Analytics, which sets no cookie (PRIVACY.md).
81
+ - **Roles in the dashboard.** What each person may do in a workspace is one of six roles (Owner,
82
+ Admin, Developer, Member, Viewer, Billing), read from the database on every request
83
+ (`slotlock.workspace_memberships`, `createSlotlockDashboardWorkspaces`), so a role changed or a
84
+ membership ended applies at the next request. Every dashboard route needs a capability
85
+ (`workspace-roles.ts`); one with none is refused. A person is the Owner of their own workspace;
86
+ in an organization's, GitHub's owners are its Owners and its members Developers.
87
+ - **Confirm it's you.** Full-access keys, webhooks, billing and the assistant's model need a
88
+ sign-in in the last 15 minutes; past it, the dashboard asks the person to sign in again and
89
+ returns to the page.
90
+ - **Sign out everywhere** (Settings, Account) ends every session of the person, on every device
91
+ (a per-person session epoch, `slotlock.dashboard_people`).
92
+ - **An audit log** (`slotlock.audit_events`, 180 days): key, webhook, billing, assistant-model,
93
+ contact and session changes, and every refusal, with where they came from. The serving role may
94
+ only add to it and read it (`SLOTLOCK_APPEND_ONLY_TABLES`); maintenance prunes it.
95
+ - **Team workspaces** (`SLOTLOCK_DASHBOARD_TEAM_WORKSPACES=on`): anyone signed in creates a
96
+ workspace of its own (`team:<id>`) on the Teams page, invites people to it by email with a role,
97
+ and changes their roles, removes them (revoking the keys they made, or keeping them) or leaves on
98
+ Settings, Members. An invitation's link works once, for seven days, only for the address it was
99
+ sent to (verified by Google, or by GitHub when the person confirms it), and opening it consumes
100
+ nothing. A team always keeps an Owner; nobody gives a role above their own. `serve` takes a
101
+ team's keys only with the setting on and while the team exists. Where plans apply, each person
102
+ creates one team on Free, which holds up to 3 people. `SlotlockInvitationSender` sends the
103
+ links by email where a deployment has one; otherwise the person inviting copies it.
104
+ - Google's `email_verified` is accepted as the string `"true"` as well as `true`.
105
+ - **The audit log in the dashboard** (Settings, Audit log): Owners and Admins read who did what,
106
+ refused or done, from where, 50 events a page, filtered by action, and export it as CSV or JSON on
107
+ the Team plan. Each tenant's events form a hash chain (`chain_index`, `prev_hash`, `hash`), with
108
+ the time stamped by the database: an event changed, removed or taken off the chain in the middle
109
+ shows where (`slotlock.verify_audit_chain`, `verifyAuditChain`); exports carry each hash, to keep
110
+ as proof elsewhere. Retention removes only the start of the chain.
111
+ - **Where you're signed in** (Settings, Account): each session's browser, address, start and last
112
+ use, and signing another device out at its next request (`slotlock.dashboard_sessions`, a digest
113
+ of each session's id; `SlotlockDashboardSessions`). A session passes only while its record does,
114
+ so sessions from before this release sign in once more, and a person's 50 most recent are kept.
115
+ - **Team for team workspaces**: a team's Owners and Billing managers buy the Team plan on Usage, as
116
+ an organization's do.
117
+ - **Deleting a team** (Settings, Team): an Owner types its handle; its keys stop (on every server
118
+ within 30 seconds), every member sees the date, and maintenance erases it seven days later unless
119
+ an Owner keeps it (`slotlock.schedule_team_deletion`, `cancel_team_deletion`,
120
+ `erase_deleted_teams`). Refused while the team has a subscription, and never erased while it has
121
+ one.
122
+ - **Invitations by email**: `SLOTLOCK_INVITATION_EMAIL` (`resend` or `postmark`), with
123
+ `SLOTLOCK_INVITATION_EMAIL_KEY` and `SLOTLOCK_INVITATION_EMAIL_FROM`
124
+ (`createSlotlockInvitationEmailSender`). Each person is held to 50 invitations a day across their
125
+ teams, as each team is; sending one again counts as a send. One address is sent at most 5 a day
126
+ from every team together (`recipient_limit`), so open sign-up cannot fill an inbox. Requests to
127
+ the provider carry a User-Agent (Resend refuses one without), and a refusal is logged with the
128
+ provider's status (`SlotlockInvitationEmailError`), never the address.
129
+ - **Requests about one person** (RFC 0004 D3). `POST /v1/subjects/export` (`exportSubject`,
130
+ `slotlock subject export`) lists what the tenant keeps about one person, found by exactly one of
131
+ `email`, `holder`, `contact` or `reference`: the rows that name them, by table
132
+ (`SLOTLOCK_SUBJECT_TABLES`). `POST /v1/subjects/erase` (`eraseSubject`,
133
+ `slotlock subject erase`) takes them out in one transaction: their contacts, attendee entries and
134
+ waitlist entries are deleted, their email leaves events' attendee and organizer lists and
135
+ approvals' requests, and their holder and booking references become one `erased-<32 hex>` name
136
+ in holds, bookings, cancelled bookings, events, approvals and the change history. Bookings stay
137
+ as time taken; nothing goes on the feed; the audit log records `subject.erased` without the
138
+ person.
139
+ - **Privacy in the dashboard** (Settings, Privacy; Owners and Admins, with a sign-in in the last 15
140
+ minutes: the new capability `privacy.manage`). Export the workspace and download it as JSON
141
+ lines; find one person by email, holder, contact or booking reference, see how many rows of each
142
+ table name them, download them as JSON, or erase them once confirmed. Every form posts, so the
143
+ person is never in an address; the audit log records `export.created`, `export.downloaded`,
144
+ `subject.exported` and `subject.erased` without them.
145
+ - **People limited to locations or groups** (RFC 0004 D1). On a team's Members page an Owner, or an
146
+ Admin not limited themselves, limits anyone but an Owner to up to 20 locations and 20 groups
147
+ (new column `workspace_memberships.resource_scope`; definers `slotlock.member_scope`,
148
+ `slotlock.member_scopes` and `slotlock.set_member_scope`). From their next request every
149
+ transaction the dashboard runs for them carries that resource scope (`dashboard-scope.ts`), so
150
+ RFC 0003 G6's policies keep it to those resources; Approvals shows and decides only what asks
151
+ for theirs alone, and their assistant is limited the same way. They keep only reading, the desk
152
+ and the assistant (`SLOTLOCK_SCOPED_CAPABILITIES`, `SLOTLOCK_SCOPED_DASHBOARD_PAGES`,
153
+ `SLOTLOCK_SCOPED_DASHBOARD_POSTS`); the keys they created are revoked unless kept. Audited as
154
+ `member.scope_changed`. Made an Owner by any path, a person's limit is cleared (a trigger on
155
+ `workspace_memberships`), so it does not return if they stop being one. Custom resource adapters
156
+ declare `keepsToScope`; without it a limited person reaches only their own account. Rerun
157
+ `slotlock migrate` and the role grants.
158
+ - **Exporting a whole tenant** (RFC 0004 D3). `POST /v1/tenant/exports` asks for a copy of
159
+ everything the tenant keeps, read through one cursor so at one moment; the server builds it after
160
+ answering into parts of one table's rows (new tables `tenant_exports` and `tenant_export_parts`),
161
+ read with `GET /v1/tenant/exports/{export_id}/parts/{part}` and kept 24 hours. Secrets stay out;
162
+ derived, internal and per-member tables are left out (`SLOTLOCK_TENANT_EXPORT_TABLES`,
163
+ `SLOTLOCK_TENANT_EXPORT_LEFT_OUT`). One at a time, five kept, 512 MB at most; maintenance deletes
164
+ expired exports and builds any left pending. `slotlock tenant export [--out <dir>]` reads the same
165
+ snapshot straight to NDJSON, storing nothing. Recorded in the audit log (`export.created`).
166
+ - **The audit log over the API and the command line** (RFC 0004 D2): `GET /v1/audit` (on the Team
167
+ plan where plans apply, as the dashboard's export) and `GET /v1/audit/verify`;
168
+ `slotlock audit export` and `slotlock audit verify` (exit 1 when the chain breaks).
169
+ - **Verified domains** (`SLOTLOCK_VERIFIED_DOMAINS=on`, with team workspaces): a team's Owners add
170
+ their organization's domain on Settings, Identity and publish a TXT record
171
+ (`_slotlock-challenge.<domain>`, a 128-bit token per claim). The server's maintenance looks it up
172
+ daily over DNS-over-HTTPS, Cloudflare's cross-checked by Google's (`SLOTLOCK_DNS_RESOLVER=system`
173
+ uses the server's own resolver); a record gone on two checks a day apart makes the domain lapse
174
+ in 14 days, with a new token, so a domain that changes hands cannot be revived from DNS history.
175
+ One team holds a domain at a time; public suffixes, special-use names and consumer mail domains
176
+ are refused (the Public Suffix List through `tldts`, pinned). People whose verified address is at
177
+ a team's domain find it on the Teams page and ask to join, approved by an Owner or Admin as
178
+ Viewer, Member or Developer; an Owner may let Google Workspace accounts of the domain join at once
179
+ as a Viewer (an address verified once by Google or GitHub, or one the team removed or declined,
180
+ only asks), or turn joining off. A record behind a CNAME does not count, and 14 days of resolver
181
+ errors lapse a domain too.
182
+ - **Single sign-on through each team's own identity provider** (`SLOTLOCK_TEAM_SSO=on`, with
183
+ verified domains, and `SLOTLOCK_SSO_SECRET`): Okta, Microsoft Entra ID (one tenant), Google
184
+ Workspace or any OpenID Connect provider, set up, tested and turned on by the team's Owners on
185
+ Settings, Identity. ID tokens are checked with WebCrypto on both runtimes (`verifySlotlockIdToken`:
186
+ RS256, PS256 or ES256, never none or HMAC; RSA of 2048 bits or more; exact issuer; audience and
187
+ azp; time with 60 seconds' skew; nonce), keys cached per JWKS URL, an Owner's URLs fetched as
188
+ hostile (https on public names, no redirects, 5 seconds, 256 KB), client secrets sealed with
189
+ AES-256-GCM. Identities are the provider's subject, never an email; a sign-in through a provider
190
+ alone makes a person with no workspace of their own, scoped to the team (no other team, nor the
191
+ person's other sessions); members link their GitHub or Google person by signing in through it,
192
+ with a work account at the verified domain, while signed in. Members at the verified domains
193
+ join at their first sign-in (on by default, as Viewer), groups map to roles (never Owner, and
194
+ nobody who signs in only through a provider is made one), and the team may require single
195
+ sign-on, with an Owner's audited one-hour bypass. Turning a connection off or changing it ends the
196
+ sessions and proofs it gave; deleting it, or erasing the team, erases the people who signed in
197
+ only through it. Google sign-in now goes through the same verifier, which allows 60 seconds of
198
+ clock skew.
199
+ - **SCIM 2.0 provisioning for each team** (`SLOTLOCK_TEAM_SCIM=on`, with verified domains): the
200
+ team's identity provider adds, deactivates and groups its people at the dashboard's
201
+ `/scim/v2/<team id>` (RFC 7643/7644: Users and Groups with GET, POST, PUT, PATCH and DELETE,
202
+ `eq` filters, paging, discovery), with bearer tokens an Owner makes on Settings, Identity (two at
203
+ a time, kept as SHA-256), and Okta's and Entra's requests read as they send them. Users at the
204
+ verified domains only; one becomes a member on signing in with a Google Workspace account of the
205
+ domain or through the team's single sign-on. Deactivation ends that membership at the next
206
+ request and, as the Owner chose when turning SCIM on, revokes the person's keys; reactivation
207
+ needs a new sign-in. Groups give roles (`slotlock-role-<role>`, or as an Owner maps them; never
208
+ Owner), and the directory never changes an Owner. On Workers, the `SLOTLOCK_SCIM_RATE_LIMIT`
209
+ binding counts providers' calls per address apart from the dashboard's per-client budget: a
210
+ provider calls from addresses its other customers share. The dashboard's `scim` option;
211
+ `workspaces.scim`.
212
+ An Owner may also hold invitations to the team's verified domains. New capability
213
+ `identity.manage` (Owners, fresh sign-in); `createSlotlockWorkspaceDomains`,
214
+ `runSlotlockDomainChecks`, `normalizeSlotlockDomain`, `createSlotlockDohResolver`.
215
+
216
+ ### Changed
217
+
218
+ - **The change history cannot be edited** (RFC 0004 D2). The serving role may only add changes
219
+ and read them (`SLOTLOCK_APPEND_ONLY_TABLES` includes `outbox`): pruning goes through the
220
+ definer `slotlock.prune_changes`, and the one edit is taking a person out
221
+ (`slotlock.erase_subject_changes`, which writes nothing but an `erased-` name). Both act only for
222
+ the tenant the caller acts in. Rerun `slotlock migrate` and the role grants.
223
+ - **History is kept by plan**: 30 days on Free, 90 on Developer and 400 on Team
224
+ (`SLOTLOCK_CHANGE_RETENTION_BY_PLAN`), or `retentionDays` if longer; a tenant with no plan keeps
225
+ `retentionDays`. The workspace audit log keeps 180 days, or 400 on Team. The privacy notice says
226
+ so, and how an account holder answers a person's request themselves.
227
+ - **The privacy notice lists contacts, busy time from other calendars, and payments for holds**,
228
+ with how long each is kept: a contact until it is deleted; busy time 30 days after it ends; a
229
+ payment record until its resource or account goes. It says hosted Slotlock sends nothing to
230
+ contacts, holds no calendar or payment provider's token, and that the integrations that do run
231
+ under the tenant's own agreements. Erasure lists all three.
232
+ - **An organization's members no longer have every power.** Before, any member could create a
233
+ Full-access key (which reaches contacts, webhooks, history and erasure), revoke anyone's key and
234
+ add webhooks. Members are now Developers: read-only and read-write keys of their own, resources,
235
+ rules and hours. Review the Full-access keys your organization's members created.
236
+
237
+ ### Security
238
+
239
+ The four questions the review left open, now closed:
240
+
241
+ - **Moving a booking no longer avoids a late cancellation.** Where the reschedule cutoff was
242
+ shorter than the cancel cutoff, a booking could be moved far ahead and then cancelled free. A
243
+ booking or event moved when cancelling it would already be late now keeps the start it had
244
+ (`cutoff_anchor`), and a later cancellation is judged, and its fee recorded, from it.
245
+ - **An agent no longer sets its own hold price.** The new booking rule `holdPrice` (`hold_price`)
246
+ is the tenant's price: an agent's hold of the resource waits for it whatever the agent asks, a
247
+ `payment` it names that is not the price is refused (`payment_price_mismatch`), and an agent
248
+ books the resource only from a paid hold. Without a price, an agent names none.
249
+ - **A key's caps count its pending approvals.** The time an agent's approvals hold while a person
250
+ decides counts against its key's `max_holds` and `max_held_hours`, so asking for approval never
251
+ holds more than the agent could hold itself (`hold_limit_reached`, `held_time_limit_reached`).
252
+ - **ISK and UGX in `@slotlock/payments-stripe`.** Stripe counts them in hundredths ISO 4217 has
253
+ not, so 1 krona paid read as 100 against a hold's price. The adapter converts
254
+ (`isoMinorUnits`), refuses an amount that is not whole, and `stripeAmount` gives the
255
+ PaymentIntent's.
256
+
257
+ From a review of RFC 0004's T2 changes, in five areas, each finding reproduced against a real
258
+ database first. This was not a human review, so it does not count as the independent review
259
+ GOVERNANCE.md requires.
260
+
261
+ - **Approvals could be got round by an update.** An agent's update that kept its event's time and
262
+ resource skipped every policy. A transparent event made opaque, or a granted booking given a
263
+ recurrence rule, booked that time unasked. A granted approval's idempotency key also booked any
264
+ other bundle. Updates are now judged by the time they would add, and a key answers only the
265
+ request a person approved.
266
+ - **A 129-byte holiday calendar could hang the server.** A daily rule on a day that never comes held
267
+ it for every tenant, again after each restart once subscribed. A 1 MB calendar held it for a
268
+ minute. Such rules are refused, a calendar's work is bounded, and a subscription due for import
269
+ is put off as it is taken. A subscription named after its URL shows the host only.
270
+ - **Payments a hold never used could be kept.** These were not refunded: a late payment for a
271
+ hold already cleared away, a refund Stripe did not make, and a paid hold that expired or was let go
272
+ before its booking. Slotlock now records whether a paid hold was booked. It answers the payment for
273
+ one that ended unbooked with `hold_ended`, and says so on the feed (`payment_unapplied`).
274
+ `@slotlock/payments-stripe` names the tenant on each PaymentIntent and refunds each such payment,
275
+ retrying a refund that failed. It logs at level error each payment it keeps.
276
+ - **An agent could let a booking go past its cutoffs another way:** by making it transparent, or a
277
+ series. Both are now refused as the cancel or move they are.
278
+ - **Busy time from calendars missed a composite's parts, and two booking paths.** A hall's busy time
279
+ is now counted on its rooms and a room's on the hall. A hold's confirmation and a series' roll
280
+ refuse it too. Busy time that ended a month ago goes.
281
+ - **A `@slotlock/sync` connect link could be reused** by anyone who had it for 30 minutes. A link is
282
+ now followed once, and its flow is finished only in the browser that followed it.
283
+ - **Reminders already sent were sent again when a reminder list changed.** Each sent reminder was
284
+ kept by its place in the list, so adding, removing or reordering one sent the earlier ones twice
285
+ (an SMS or email through `@slotlock/notify`). They are now kept by when they were due: a changed
286
+ list sends only what is new, and a moved booking is reminded again as before.
287
+ - **`@slotlock/sync`, hardened further:**
288
+ - One calendar that refuses for good (read only, access revoked) no longer stops write-back for
289
+ the whole tenant: it is that mapping's failure (`failedMappings`), and read-only calendars are
290
+ not offered for write-back.
291
+ - A connection disconnected during a pass no longer comes back with its tokens, nor records busy
292
+ time.
293
+ - Seals are bound to their tenant, and a mapping uses only its own tenant's connection.
294
+ Connections made before this must be connected again.
295
+ - iCalendar feeds are read on core's worker threads, with a deadline.
296
+ - Google connections hold only the scopes they asked for, and one missing a scope is refused.
297
+ - A library user who reads iCalendar URLs must pass the guarded fetch.
298
+
299
+ ## 0.9.1 - 2026-11-11
300
+
301
+ ### Fixed
302
+
303
+ - **The dashboard asks an owner to upgrade where a plan stops them.** A workspace at a limit of its
304
+ plan (10 of 10 resources on Free, say) saw only a red bar: Usage and the Overview now say
305
+ "On Free, you have 10 of 10 resources: adding another is refused", with **Upgrade**, and the
306
+ Overview's plan card offers it too. The same holds for the plan a workspace is about to move to.
307
+ An add refused at a limit (a resource, a key, a webhook) links to the upgrade, or to Usage for a
308
+ workspace that pays. A quota at 80% offers the upgrade as well as at 100%. A member is told an
309
+ owner can upgrade, and where the deployment neither sells nor arranges plans, no upgrade is named.
310
+
311
+ ## 0.9.0 - 2026-11-11
312
+
313
+ ### Added
314
+
315
+ - Each hold's payment is kept apart from the hold (new table `hold_payments`, under RLS, erased with
316
+ the tenant), so `slotlock_mark_hold_paid` with the same payment answers `replayed: true` however
317
+ the hold ended: confirmed in place or booked as an event. Before, a confirmed hold answered
318
+ `404 hold_not_found`, which `@slotlock/payments-stripe` with `refundRefused` refunded: a customer
319
+ reloading the return page, or Stripe redelivering a webhook, got a booked time refunded (found by
320
+ the security audit). The adapter now refunds only `hold_expired`, `payment_conflict` and
321
+ `payment_amount_mismatch`.
322
+ - Three integration packages in `packages/` (RFC 0004 P3), each its own process with its own
323
+ schema and secrets, talking to Slotlock over its API:
324
+ - `@slotlock/sync` (C1, C4, T2): busy time from Google freeBusy, Microsoft getSchedule and
325
+ iCalendar URLs, recorded as each source's busy time; confirmed bookings written back to Google
326
+ and Microsoft calendars, times only. OAuth with PKCE, then a signed page where the tenant's person
327
+ chooses which calendar blocks which resource; tokens, URLs and keys sealed with AES-256-GCM. A
328
+ booking changed in the calendar is reported on the feed (`coverage.conflict`), never applied. A
329
+ provider's 429 or 503 is retried when it says, else with backoff; a feed cursor that expired
330
+ while the sync was down is recovered by reconciling each calendar with the bookings as they are.
331
+ - `@slotlock/notify` (C2): confirmations, moves, cancellations and reminders by Postmark,
332
+ Resend or SES, and Twilio SMS and WhatsApp, each send recorded under its key (change id,
333
+ channel, recipient) before it is made. Consent per channel; test tenants go to a capture inbox.
334
+ Recipients are kept only as digests, except an event's attendees, kept sealed until a day after
335
+ it ends so a cancellation reaches them.
336
+ - `@slotlock/payments-stripe` (C3, T2): a PaymentIntent read back from the tenant's own Stripe
337
+ account marks its hold paid, with the amount received; a payment for an expired hold is refused
338
+ and, if asked, refunded.
339
+ - Each runs as its own process (`slotlock-sync`, `slotlock-notify`, `slotlock-payments-stripe`,
340
+ each with `migrate` and `serve`), and `packages/integrations.Dockerfile` builds one image with
341
+ all three.
342
+ - What integrations build on (RFC 0004 P3, D1's recommendation: core keeps primitives, packages
343
+ outside it send and sync). Feed type `reminder.due`: maintenance puts each event reminder on the
344
+ feed once, when it comes due (within the last day, before the event starts), and again if the
345
+ occurrence moves; `reminders_due` in its answer. A change's `id` is the key a sender sends it
346
+ once by. Events take `notify` (`slotlock_create_event`'s `notify: false`): no reminders, and a
347
+ sender tells no one. Reservations take `reminder_minutes` (up to five, minutes before the start),
348
+ each said once as `reminder.due` for the contact the reservation names, and again if it moves.
349
+ - Contacts (P3, T2): new table `contacts`, the people a booking may name to be told about it, by
350
+ the tenant's own reference, with email, phone and WhatsApp numbers (E.164), locale and consent
351
+ per channel. `slotlock_put_contact`, `slotlock_get_contact`, `slotlock_delete_contact`
352
+ (`/v1/contacts/{contact_ref}`); deleting one also takes its reference off every booking. At most
353
+ 100,000 a tenant. `slotlock_list_contacts` (`GET /v1/contacts`) lists them a page at a time, and
354
+ each answers `consent_at`: when each channel's answer was given, kept while it stands. The
355
+ dashboard's Contacts page lists them and forgets one, for an erasure request. Reservations take
356
+ `contact_ref` and `notify`, both on the feed with each change; neither is ever the person.
357
+ - Holds that wait for payment (RFC 0004 C3, T2): `payment_required` on `slotlock_acquire_hold`
358
+ (time to live at most 30 minutes). Confirming one answers `409 payment_required` until
359
+ `slotlock_mark_hold_paid` (`POST /v1/holds/{hold_id}/paid`) records the payment's reference at
360
+ the integrator's processor: the same reference again answers the same hold, another
361
+ `payment_conflict`, an expired hold `hold_expired`. New feed type `hold.paid`. Slotlock records
362
+ payment state and never moves money. A hold may name what the payment must be (`payment`:
363
+ `amount_minor` and `currency`); `slotlock_mark_hold_paid` then takes what was `received`, and
364
+ less, or another currency, is `409 payment_amount_mismatch`. Holds answer `payment_amount` and
365
+ `paid_amount`. Agents' `slotlock_hold_slots` takes `payment_required` and `payment` too.
366
+ - Busy time from calendars that give no events (RFC 0004 C1, T2): `slotlock_put_source_busy`
367
+ (`PUT /v1/resources/{resource_id}/coverage/{source}/busy`) records a source's coverage of a
368
+ window and the busy time in it (at most 5,000 intervals, merged), replacing what it said of that
369
+ window. New table `source_busy`. Every read counts it (free/busy, a resource's busy time,
370
+ searches and waitlist offers), and every booking Slotlock makes is refused
371
+ over it (`overlap`). A confirmed booking the source newly reports busy time over is left alone
372
+ and reported once as `coverage.conflict` (named by `source_id`, never the source). Retiring the
373
+ source forgets its busy time. `slotlock_report_coverage_conflict`
374
+ (`POST /v1/resources/{resource_id}/coverage/{source}/conflicts`) lets a calendar sync report a
375
+ booking it wrote that was moved (`changed_in_calendar`) or removed (`removed_from_calendar`) in
376
+ the calendar, as `coverage.conflict` with the booking left as it is.
377
+ - Hours that inherit (RFC 0004 B2). The tenant and each location store weekly hours
378
+ (`setTenantAvailability`, the new `setLocationAvailability`; `PUT /v1/availability`,
379
+ `PUT /v1/locations/{location}/availability`, `GET /v1/availability`). A resource without its own
380
+ uses its location's (the nearest up the chain), then the tenant's, then the server's, and a
381
+ resource added later inherits too. Resources carry `inherited_availability`. The dashboard has
382
+ "Your hours" and an Hours page for each location; the CLI has `hours set --location`.
383
+ - Closures and special days (RFC 0004 B1, T2): new table `availability_exceptions`, for the tenant,
384
+ a location (and everything inside it) or one resource. Each replaces whole local days, read in
385
+ each resource's time zone: `closed` empties them, `open` gives them 1-4 windows of their own.
386
+ The nearest scope wins on a day. One evaluator (`openWindows`) serves search, pool availability,
387
+ approval policies' `outside_hours`, and writes under `enforceHours`. At most 5,000 a tenant
388
+ (`availability_exceptions_limit_reached`). API: list, create and delete under
389
+ `/v1/availability/exceptions`. Dashboard: a Closures page. CLI: `slotlock closures
390
+ list|add|delete|import`.
391
+ - Holiday calendars (B1): `slotlock_import_holidays` (`POST /v1/availability/holidays`) and
392
+ `closures import` close the days an iCalendar file's all-day events cover, recurring ones
393
+ expanded over a bounded span. `serve` fetches an `https` URL through the guarded fetch (no
394
+ private addresses, 1 MB, 10 seconds); the store-backed application backend fetches none unless
395
+ given `holidayFetch`. Each import is a calendar (new table `holiday_calendars`, under RLS, erased
396
+ with the tenant), kept by name where it applies: its `name`, else its own `X-WR-CALNAME`, else
397
+ the URL. Importing it again updates its closures and removes those it no longer names in the
398
+ span (`removed`), so a holiday a calendar dropped goes too; a closure imported before calendars
399
+ were kept becomes the calendar's that names it. With `subscribe`, the URL is kept and maintenance
400
+ imports it again every 30 days (`holidayFetch` on `maintainSlotlockTenant` and the scheduler;
401
+ `serve` and `slotlock maintain` pass the guarded fetch), a failed import keeping its closures,
402
+ noting why (`last_error`) and trying again a day later. `slotlock_list_holiday_calendars`
403
+ (`GET /v1/availability/holiday-calendars`) and `slotlock_delete_holiday_calendar`
404
+ (`DELETE …/{calendar_id}`, its closures with it); closures answer `calendar_id`; at most 50
405
+ calendars a tenant. The dashboard's Closures page takes a file upload (the form was limited to
406
+ 8 KB, too small for most calendars, and now takes 1 MB), a paste or, where the server fetches
407
+ them (`serve`, Cloud), an address with "Import it again every 30 days", and lists the calendars
408
+ with their next import, a failed one's error, and Delete. `closures import` names the calendar
409
+ with `--label`.
410
+ - Durations and hires (RFC 0004 B3). Booking rules gain `minDurationMinutes`, `maxDurationMinutes`,
411
+ `allowedDurations` and `defaultDurationMinutes`, and for a booking from one local day to another
412
+ `pickupTimes` and `returnTimes` (it starts and ends in them, and under `enforceHours` needs only
413
+ its start and end inside the hours). `turnaroundMinutes` goes up to a week. Refusals:
414
+ `booking_rule_min_duration`, `booking_rule_max_duration`, `booking_rule_duration_not_allowed`,
415
+ `booking_rule_pickup_time`, `booking_rule_return_time`. Searches offer only allowed lengths, and
416
+ take `defaultDurationMinutes` when they name none (`400 duration_required` otherwise).
417
+ `hoursApply` (`hours_apply`) says how the hours hold a hire: `whole` (all of it) or `ends` (its
418
+ start and end only), by default `ends` with pick-up or return times, else `whole`. Writes follow
419
+ it under `enforceHours`; `slotlock_find_available_slots` and `slotlock_find_next_available`
420
+ follow it always, so with `ends` they offer slots from one day to another, judged as the write
421
+ would be, and `slotlock_find_available_ranges` judges ranges as the write would where hours are
422
+ enforced. A pick-up or return refusal comes with alternatives, as other refusals of a time do.
423
+ - Zone rule changes reach everything (RFC 0004 B9): `followZoneRules` also re-checks a recurring
424
+ series' occurrences to come (each kept with its wall time, reported once as
425
+ `event.timezone_shifted` with its `recurrence_id`; with `move`, the whole series moves at its
426
+ next revision in one transaction, each occurrence and its booking keeping its `recurrence_id`,
427
+ but only where the server's zone rules agree with the database's, else each is reported with
428
+ `reason: "rules_disagree"`; something in the way keeps its times, said once as
429
+ `event.timezone_conflict`), and reservations written with the new
430
+ optional `local_start` (`localStart`), reported as `reservation.timezone_shifted` or, with
431
+ `move`, moved at the next revision (`reservation.timezone_conflict` when something is in the
432
+ way). A `local_start` that does not name the reservation's start is `400 invalid_local_start`.
433
+ - Holds across resources (RFC 0004 B7, T2): `slotlock_hold_together` (`POST /v1/slot-holds/together`),
434
+ the 19th agent tool, holds one time on 2-20 resources all or nothing, as one group whose holds
435
+ expire together. `slotlock_book_together` takes its `hold_group_id` and books the set from those
436
+ holds in one transaction. The application API gains `slotlock_tenant_book_together`
437
+ (`POST /v1/tenant/bundles`) and `slotlock_tenant_book_pool` (`POST /v1/tenant/pool-bookings`), the
438
+ agents' bundle and pool bookings for the tenant's own systems. Their events are the tenant's
439
+ (owner `internal`), not the key's: no agent's event quota counts them, and revoking the key
440
+ leaves them. Approval policies hold them only when they say `include_application: true` (new,
441
+ false by default), and a grant books them as the tenant's again; a key that `requires_approval`
442
+ still waits for a person whatever it books.
443
+ - Waitlists that match (RFC 0004 B6): an entry may wait for any slot of a length within its
444
+ window (`duration_minutes`, `durationMs`), on a resource or a pool, and is offered the earliest
445
+ free one. Offers end `accepted`, `declined` (new `slotlock_decline_waitlist_offer`,
446
+ `POST /v1/waitlist/{entry_id}/decline`) or `lapsed`, decided by what becomes of their hold, by a
447
+ database trigger whatever path takes it. New feed types `waitlist.joined`, `.accepted`,
448
+ `.declined`, `.lapsed` and `.left`. Entries answer `duration_minutes` and `decided_at`.
449
+ - A written booking lifecycle (RFC 0004 B4, T2): `src/lifecycle.ts` names each status, the move
450
+ each action makes and the refusal every other gets, and a test holds the store to it. A
451
+ tombstone keeps why the booking ended (`end_reason`: `cancelled` or `no_show`). New feed types:
452
+ `reservation.rescheduled` (a move, as well as `reservation.updated`) and `reservation.completed`,
453
+ which maintenance writes once for each booking whose end passed in the last week
454
+ (`bookings_completed` in its answer).
455
+ - Cancellation and reschedule policies (RFC 0004 B5, T2): rules `cancelCutoffMinutes` and
456
+ `rescheduleCutoffMinutes` (`booking_rule_cancel_cutoff`, `booking_rule_reschedule_cutoff`), which
457
+ an admin may go past with `override_reason`, kept on the tombstone and the feed; and
458
+ `cancellationFee`, recorded on a late cancellation for the integrator to charge.
459
+ - `slotlock_find_available_ranges` (`GET /v1/available-ranges`), the 18th agent tool: hires across
460
+ days for named resources or a pool's members, the earliest range on each from a pick-up window,
461
+ lasting N days or ending in a return window.
462
+ - VAVAILABILITY carries RDATE and EXDATE (B1): the export adds the next year's closures as EXDATEs
463
+ and special hours as AVAILABLEs with RDATEs. The import reads them, and single AVAILABLEs, as
464
+ the resource's own exceptions, replacing those an earlier import made.
465
+
466
+ - **Chats in the dashboard assistant.** Each conversation is a chat a member keeps:
467
+ - it is titled by its first message, at its own address (`<root>/assistant?c=<id>`);
468
+ - chats are listed beside the one shown, most recent first, grouped by day (Today, Yesterday,
469
+ Previous 7 days, Previous 30 days, Older), with a search box, and **Rename** and **Delete**
470
+ behind each one's menu;
471
+ - **New chat** starts another, and on a narrow screen the list is a page of its own
472
+ (`<root>/assistant/chats`);
473
+ - the page opens on the chat used in the past two hours, else on a new one;
474
+ - a streamed reply tells the page which chat it went to, so a new one gets its address and its
475
+ place in the list without a reload.
476
+
477
+ Chats are kept until the member deletes one, at most 100 per member (`SLOTLOCK_ASSISTANT_MAX_CHATS`;
478
+ past it, the least recently used goes, even with many started at once). A chat deleted while its
479
+ reply is being written stays deleted. They live in the new table `slotlock.assistant_chats`,
480
+ under forced RLS, erased with the tenant. `slotlock migrate` makes each member's 0.8 conversation
481
+ their first chat, once. Run `slotlock migrate`.
482
+ - **Finding a resource by name.** `slotlock_list_resources` takes `query` and ranks the resources
483
+ that match what a person calls one, best first:
484
+ - each word is looked for in the name, reference, labels, kind, location and description;
485
+ - case and accents are ignored, and a typo, a word cut short or words run together are forgiven
486
+ (`mercedez`, `merc 2025` and `mercedesbenz` all find "Mercedes-Benz 2025");
487
+ - numbers match exactly;
488
+ - a search does bounded work, whatever the resources hold: past a budget of typo checks, it
489
+ matches exactly, by prefix and by words run together only.
490
+
491
+ `resource_not_found` for a name or reference given where an id belongs names the likeliest
492
+ resources in `details.candidate_1` to `candidate_3`.
493
+
494
+ ### Changed
495
+
496
+ - The tenant's hours are stored, not copied onto every resource (RFC 0004 B2), and resources
497
+ without their own inherit them, those added later too. `storeTenantAvailability` stores them and
498
+ answers `{ inheriting, ownHours }`. `setTenantAvailability` keeps its 0.8.0 contract, deprecated:
499
+ every resource uses the hours afterwards, and it answers how many. `PUT /v1/availability` keeps
500
+ the hours resources have of their own by default; its `override_resources` is `none`, `all`
501
+ (clear them) or `matching` (clear those the same as the tenant's were or as the new ones: the
502
+ copies earlier versions made, while hours set apart on purpose stay), as are
503
+ `hours set --all --override` and `--matching`. It answers `resources_updated` (resources the
504
+ hours now apply to) and `resources_with_own_hours`. The dashboard's "Your hours" names the
505
+ resources that keep their own, with the same three choices, and `/resources?hours=own` lists
506
+ them.
507
+ - `slotlock_run_maintenance` answers `bookings_completed`; the five clients' checks of its keys
508
+ include it. `slotlock_cancel_reservation` and `slotlock_cancel_tenant_event` answer
509
+ `cancellation_fee` (null when none).
510
+ - `parseVAvailability` answers `days` as well as `rules`, and no longer refuses RDATE, EXDATE or an
511
+ AVAILABLE without RRULE.
512
+
513
+ - **The dashboard assistant's store** (`SlotlockAssistantStore`) keeps chats. `listChats`,
514
+ `getChat`, `putChat`, `renameChat` and `deleteChat` replace `getConversation`,
515
+ `putConversation` and `deleteConversation`, and `settleAction` takes the chat's id. The tenant
516
+ store has `listAssistantChats`, `getAssistantChat`, `putAssistantChat`, `renameAssistantChat`
517
+ and `deleteAssistantChat`, and `settleAssistantAction` takes `chatId`.
518
+ - **Conversation lifetime.** `conversationTtlMs` no longer defaults to 2 hours: without it, chats
519
+ are kept until deleted. Given, it may be up to 3,650 days.
520
+ - **New conversation is New chat.** It starts another chat and deletes nothing. A form of 0.8 that
521
+ posts `<root>/assistant/reset` does the same.
522
+ - **The assistant's system prompt** lists the workspace's resources (up to 50, with the ids tools
523
+ take) and says how to find others. The tool descriptions and the MCP instructions say that
524
+ `resource_id` takes an id and how to find one, and that a filter leaves out resources that state
525
+ nothing.
526
+
527
+ ### Fixed
528
+
529
+ - Enforced hours (`enforceHours`) judged a write inside `withTenant` against no hours when its
530
+ resource used the server's default (`defaultAvailabilityRules`, `SLOTLOCK_AVAILABILITY` under
531
+ `serve`): every such booking was refused `booking_rule_outside_hours`. The tenant's store now
532
+ carries the server's hours.
533
+
534
+ - **The assistant's chips stream.** "What's on today" and "Find free time" were forms that waited
535
+ for the whole turn and then reloaded the page; they now send through the box, so their reply
536
+ streams.
537
+
538
+ - The dashboard assistant's Stop stops the turn on any runtime. Cancelling the streamed reply now
539
+ aborts the turn itself, which keeps what it had. Before, only `request.signal` stopped it, and a
540
+ runtime that does not abort it when the client goes kept the turn running. On Workers, the
541
+ request's database connection now closes only once the stopped turn is kept.
542
+ - The Overview never shows a change by its type. A change it has no words for, from a newer server,
543
+ reads "Something changed", on its resource when it names one. A type such as `constructor` no
544
+ longer finds an inherited entry.
545
+ - The assistant page's script keeps live steps in a map with no prototype, so a provider's tool-call
546
+ id of `__proto__` names nothing.
547
+
6
548
  ## 0.8.0 - 2026-11-10
7
549
 
8
550
  ### Added
package/NOTICE CHANGED
@@ -10,6 +10,8 @@ each retains its own license. This release was built with these versions:
10
10
  - ical.js 2.2.1, Mozilla Public License 2.0, https://github.com/kewisch/ical.js
11
11
  - rrule 2.8.1, BSD 3-Clause License, https://github.com/jakubroztocil/rrule
12
12
  - tslib (via rrule), BSD Zero Clause License (0BSD), https://github.com/Microsoft/tslib
13
+ - tldts 7.4.19, MIT License, https://github.com/remusao/tldts
14
+ - tldts-core (via tldts), MIT License, https://github.com/remusao/tldts
13
15
  - zod 4.6.7, MIT License, https://github.com/colinhacks/zod
14
16
 
15
17
  Peer dependency, installed and versioned by the application that embeds Slotlock:
package/README.md CHANGED
@@ -16,8 +16,10 @@ TypeScript library.
16
16
 
17
17
  ## Get started
18
18
 
19
- The documentation is at [slotlock.pylota.io/docs](https://slotlock.pylota.io/docs/). There are
20
- three ways to run Slotlock:
19
+ The documentation is at [slotlock.pylota.io/docs](https://slotlock.pylota.io/docs/), with a page
20
+ each for the [booking MCP server](https://slotlock.pylota.io/mcp-server/), the
21
+ [booking API](https://slotlock.pylota.io/booking-api/), [use cases](https://slotlock.pylota.io/use-cases/)
22
+ and [pricing](https://slotlock.pylota.io/pricing/). There are three ways to run Slotlock:
21
23
 
22
24
  - **[Slotlock Cloud](docs/hosted/README.md)**: nothing to run. Sign in with GitHub at
23
25
  `https://app.slotlock.pylota.io/dashboard`, create a key, and connect an agent to