slotlock 0.9.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 (61) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/NOTICE +2 -0
  3. package/README.md +4 -2
  4. package/agent-store-backend.js +209 -15
  5. package/application-api.d.ts +10 -1
  6. package/application-api.js +213 -2
  7. package/availability-store.js +13 -5
  8. package/booking-rules.d.ts +11 -0
  9. package/booking-rules.js +21 -1
  10. package/config.d.ts +50 -0
  11. package/config.js +107 -0
  12. package/dashboard-scope.d.ts +7 -0
  13. package/dashboard-scope.js +21 -0
  14. package/dashboard.d.ts +97 -2
  15. package/dashboard.js +3068 -284
  16. package/ddl-identity.d.ts +25 -0
  17. package/ddl-identity.js +881 -0
  18. package/ddl-scim.d.ts +5 -0
  19. package/ddl-scim.js +740 -0
  20. package/ddl-sso.d.ts +15 -0
  21. package/ddl-sso.js +1033 -0
  22. package/ddl.d.ts +40 -9
  23. package/ddl.js +2123 -49
  24. package/domains.d.ts +117 -0
  25. package/domains.js +340 -0
  26. package/event-feed.d.ts +1 -1
  27. package/event-feed.js +6 -34
  28. package/holiday-import.d.ts +1 -1
  29. package/holiday-import.js +2 -3
  30. package/holidays.js +70 -11
  31. package/index.d.ts +12 -4
  32. package/index.js +8 -2
  33. package/invitation-email.d.ts +26 -0
  34. package/invitation-email.js +73 -0
  35. package/maintenance.d.ts +7 -2
  36. package/maintenance.js +15 -1
  37. package/oidc.d.ts +135 -0
  38. package/oidc.js +452 -0
  39. package/openapi.json +1886 -6
  40. package/package.json +2 -1
  41. package/scim-server.d.ts +19 -0
  42. package/scim-server.js +243 -0
  43. package/scim.d.ts +180 -0
  44. package/scim.js +416 -0
  45. package/self-host.d.ts +19 -6
  46. package/self-host.js +262 -7
  47. package/slotlock.manifest.json +1 -1
  48. package/store.d.ts +273 -2
  49. package/store.js +984 -72
  50. package/types.d.ts +33 -1
  51. package/types.js +68 -1
  52. package/workspace-domains.d.ts +146 -0
  53. package/workspace-domains.js +249 -0
  54. package/workspace-roles.d.ts +47 -0
  55. package/workspace-roles.js +304 -0
  56. package/workspace-scim.d.ts +89 -0
  57. package/workspace-scim.js +157 -0
  58. package/workspace-sso.d.ts +131 -0
  59. package/workspace-sso.js +180 -0
  60. package/workspaces.d.ts +240 -0
  61. package/workspaces.js +433 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,311 @@
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
+
6
311
  ## 0.9.0 - 2026-11-11
7
312
 
8
313
  ### 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