slotlock 0.2.0 → 0.5.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.
- package/CHANGELOG.md +582 -0
- package/CONTRIBUTING.md +35 -8
- package/GOVERNANCE.md +4 -2
- package/MAINTAINERS.md +3 -2
- package/NOTICE +4 -4
- package/README.md +498 -65
- package/SECURITY.md +31 -2
- package/SPEC.md +142 -15
- package/agent-server.d.ts +62 -10
- package/agent-server.js +577 -105
- package/agent-store-backend.d.ts +12 -0
- package/agent-store-backend.js +1115 -55
- package/api-keys.d.ts +11 -3
- package/api-keys.js +29 -6
- package/application-api.d.ts +35 -1
- package/application-api.js +683 -12
- package/booking-rules.d.ts +84 -0
- package/booking-rules.js +149 -0
- package/config.d.ts +112 -0
- package/config.js +225 -0
- package/dashboard.d.ts +67 -5
- package/dashboard.js +1012 -140
- package/ddl.d.ts +45 -3
- package/ddl.js +1124 -13
- package/engine.d.ts +5 -0
- package/engine.js +23 -6
- package/event-feed.d.ts +178 -0
- package/event-feed.js +435 -0
- package/event-schema.d.ts +59 -0
- package/event-schema.js +85 -0
- package/ical-worker-thread.js +4 -2
- package/ical-worker.d.ts +8 -5
- package/ical-worker.js +9 -2
- package/index.d.ts +12 -6
- package/index.js +9 -4
- package/interop.d.ts +117 -0
- package/interop.js +356 -0
- package/maintenance.d.ts +27 -5
- package/maintenance.js +92 -28
- package/mcp-app.d.ts +7 -1
- package/mcp-app.js +259 -40
- package/metrics.d.ts +24 -0
- package/metrics.js +167 -0
- package/node-server.d.ts +9 -1
- package/node-server.js +7 -2
- package/openapi.json +10970 -0
- package/package.json +3 -2
- package/rate-limit.d.ts +80 -0
- package/rate-limit.js +741 -0
- package/rest-api.d.ts +22 -0
- package/rest-api.js +166 -11
- package/rules.js +6 -3
- package/self-host.d.ts +45 -8
- package/self-host.js +616 -46
- package/slotlock-manifest.schema.json +7 -1
- package/slotlock.manifest.json +8 -2
- package/store.d.ts +647 -28
- package/store.js +2982 -327
- package/sync.d.ts +43 -2
- package/sync.js +1515 -323
- package/timezone.d.ts +58 -3
- package/timezone.js +249 -20
- package/types.d.ts +300 -9
- package/types.js +0 -2
- package/webhooks.d.ts +91 -0
- package/webhooks.js +300 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,588 @@
|
|
|
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.5.0 - 2026-10-28
|
|
7
|
+
|
|
8
|
+
### Documentation
|
|
9
|
+
|
|
10
|
+
- The production checklist advises a `transaction_timeout` on the serving role (PostgreSQL 17+),
|
|
11
|
+
so a caller's transaction a store call runs inside cannot hold its locks and snapshot without
|
|
12
|
+
bound; on 16, `idle_in_transaction_session_timeout` and `statement_timeout`. A test pins the
|
|
13
|
+
setting on the CI's PostgreSQL 17 and 18.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- How long history is kept is a setting: `SLOTLOCK_RETENTION_DAYS` (`retentionDays` in
|
|
18
|
+
`createSlotlockStore`), 1 to 3,650 days, 30 by default. It sets how long the change feed is kept
|
|
19
|
+
and how far back a cursor reaches, and ended waitlist entries; above 30 days, retired idempotency
|
|
20
|
+
answers and booking references too (never under 30). `slotlock maintain` reads it as `serve` does.
|
|
21
|
+
- A feed cursor answers `cursor_expired` exactly when a change after it has been deleted, by this
|
|
22
|
+
server's maintenance or any other (an older server's in a rolling deploy, an operator's): a trigger
|
|
23
|
+
on the feed records how far each tenant's was pruned (`slotlock.outbox_pruned`, created once and
|
|
24
|
+
started at each tenant's oldest kept change). How old a cursor is no longer matters, so a server
|
|
25
|
+
keeping changes longer than the one that pruned them never reads past a gap unseen, and a reader
|
|
26
|
+
that is up to date stays readable. Maintenance prunes the oldest changes in feed order, never past
|
|
27
|
+
one it keeps or a transaction still running, so a reader starting over always reads on.
|
|
28
|
+
- One holder's holds are bounded in time as well as number: `maxHeldHoursPerHolder` in
|
|
29
|
+
`createSlotlockStore` refuses a hold that would bring the holder's live holds past that many
|
|
30
|
+
hours together (`held_time_limit_reached`), and `maxHoldsPerHolder` sets the count that was fixed
|
|
31
|
+
at 20. `slotlock serve` sets them with `SLOTLOCK_LIMITS` (`holder_holds`, 20;
|
|
32
|
+
`holder_held_hours`, 2,160).
|
|
33
|
+
- `slotlock_get_pool_availability` (`GET /v1/pool-availability`): how many of a pool's members are
|
|
34
|
+
free across a window, the window split where that number changes, with coverage certainty, so an
|
|
35
|
+
agent can say "three vans free 9 to 11, one after" before booking with `slotlock_book_pool`.
|
|
36
|
+
- Prometheus metrics: with `SLOTLOCK_METRICS_TOKEN`, `slotlock serve` answers `/metrics` to that
|
|
37
|
+
bearer token: calls by protocol, operation and outcome with their duration, confirmations,
|
|
38
|
+
subscriptions, refusals, maintenance and webhook deliveries, labelled by closed sets only (never a
|
|
39
|
+
tenant, key or client). `createSlotlockMetrics` counts the same from a server's own hooks.
|
|
40
|
+
- The booking rule `requireCertain` (`require_certain`): a booking must lie in time every calendar
|
|
41
|
+
sync recording coverage for the resource has read within a day, or it is refused (`uncertain`,
|
|
42
|
+
`booking_rule_uncertain` to an agent), rather than booked over what a stale or failing sync may
|
|
43
|
+
hide. Searches on such a resource, the store's and the agents', require the same coverage, of the
|
|
44
|
+
slot and its turnaround, within the same day. The dashboard's rules form has it.
|
|
45
|
+
- `DELETE /v1/resources/{id}/coverage/{source}` (`slotlock_delete_coverage`, `deleteCoverage`)
|
|
46
|
+
retires a calendar sync: free/busy and `requireCertain` stop waiting on its coverage.
|
|
47
|
+
|
|
48
|
+
- The dashboard is an app with a sidebar: an Overview with the coming week's bookings, events and
|
|
49
|
+
live holds across every resource and the agents' latest changes, named by key; Resources; API
|
|
50
|
+
keys, with the endpoints; and Settings. `SLOTLOCK_DASHBOARD_NAME` (`name` in
|
|
51
|
+
`createSlotlockDashboard`) names the deployment. Works without JavaScript, light and dark.
|
|
52
|
+
- Dashboard pages for 0.4's features: a page per resource (bookable hours, booking rules, pool, its
|
|
53
|
+
coming bookings and activity), Waitlist (entries and their offers; removing one offers its time
|
|
54
|
+
on) and Webhooks (add with the secret shown once, delete, re-enable, and each endpoint's health:
|
|
55
|
+
delivering, failing since when and why, or disabled). `SlotlockDashboardResources` takes them as
|
|
56
|
+
optional `setRules`, `setPool`, `waitlist` and `webhooks`; a page shows only when its adapter
|
|
57
|
+
offers it.
|
|
58
|
+
- `listAgenda` reads what occupies a tenant's resources in a window, every resource at once:
|
|
59
|
+
bookings, calendar events' occurrences and live holds. `listRecentChanges` reads a tenant's
|
|
60
|
+
latest changes, newest first. `SlotlockDashboardResources` takes both as optional `agenda` and
|
|
61
|
+
`recentChanges`.
|
|
62
|
+
|
|
63
|
+
- Organizations in the dashboard: with `SLOTLOCK_DASHBOARD_ORGS` (GitHub organization ids), a
|
|
64
|
+
tenant is an organization, `github-org:<id>`, that its members share. Sign-in asks for `read:org`
|
|
65
|
+
and reads the person's active memberships as an owner or member (not a billing manager); a member
|
|
66
|
+
of several served organizations switches between them, and someone in none is refused. A form
|
|
67
|
+
works only for the organization it was opened in, and the key list says who made each key.
|
|
68
|
+
Removing an organization from the list signs its sessions out and stops its keys in `slotlock
|
|
69
|
+
serve`, which reads the list whether or not it serves the dashboard itself. `createSlotlockDashboard` takes
|
|
70
|
+
`organizations`.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- A key created in the dashboard records the person who created it (`github:<user id>`) as
|
|
75
|
+
`createdBy`, not the tenant; for a personal tenant the two are the same.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- Time an agent frees is offered to the waitlist at once, as the application API's cancels already
|
|
80
|
+
were: an agent releasing its holds (`slotlock_release_slots`), deleting or moving an event, and a
|
|
81
|
+
booking moved through `PUT /v1/reservations`. Before, it waited for the next maintenance pass, up
|
|
82
|
+
to an hour.
|
|
83
|
+
|
|
84
|
+
## 0.4.1 - 2026-10-28
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- The Python client is published to PyPI again. 0.4.0's PyPI job could not build it: hatchling, the
|
|
89
|
+
build backend, now needs `tomlkit`, which the release's pinned build requirements did not list.
|
|
90
|
+
0.4.1 is otherwise 0.4.0; the other packages are released at 0.4.1 with no change.
|
|
91
|
+
|
|
92
|
+
## 0.4.0 - 2026-10-28
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
|
|
96
|
+
- Waitlists: `POST /v1/waitlist` (`slotlock_join_waitlist`, `joinWaitlist`) waits for a window on a
|
|
97
|
+
resource or in a pool; `GET /v1/waitlist` and `DELETE /v1/waitlist/{entry_id}` list and leave it.
|
|
98
|
+
Freed time is offered in the order entries joined, as a hold for the waiter (`waitlist.offered` on
|
|
99
|
+
the feed): at once after a cancel or release through the API, and by maintenance otherwise, which
|
|
100
|
+
also expires entries whose window started (`waitlist.expired`).
|
|
101
|
+
- Check-in and no-show release: the booking rule `checkInMinutes` (`check_in_minutes`) releases a
|
|
102
|
+
booking nobody claimed that long after its start (`reservation.no_show`), for bookings made once
|
|
103
|
+
the rule is set; `POST /v1/reservations/{reservation_ref}/check-in` (`slotlock_check_in_reservation`,
|
|
104
|
+
`checkInReservation`) claims one from an hour before its start (`reservation.checked_in`); a
|
|
105
|
+
move to another start clears it, and a walk-in's window runs from when it was booked. Maintenance results gain `noShows`
|
|
106
|
+
and `waitlistOffers` (`no_shows`, `waitlist_offers`). Run `slotlock migrate`.
|
|
107
|
+
- Calendar interchange: `GET`/`PUT /v1/resources/{resource_id}/availability/ical`
|
|
108
|
+
(`slotlock_export_resource_availability`, `slotlock_import_resource_availability`) write and read
|
|
109
|
+
a resource's weekly bookable hours as an iCalendar VAVAILABILITY (RFC 7953); an import must be in
|
|
110
|
+
the resource's zone or floating, and anything weekly hours cannot say answers 400
|
|
111
|
+
`invalid_availability`. `GET /v1/tenant/events/{event_id}/jscalendar`
|
|
112
|
+
(`slotlock_export_tenant_event`) answers an event as a JSCalendar Event (RFC 8984). The library
|
|
113
|
+
exports `emitVAvailability`, `parseVAvailability` and `toJSCalendarEvent`.
|
|
114
|
+
- `npm run bench` (`scripts/bench.mjs`, `bench/*.sql`): RFC 0002's pgbench workloads, Slotlock's
|
|
115
|
+
range constraint against one row per 15-minute slot, reproducible on any scratch database. A new
|
|
116
|
+
Scaling page covers partitioning by resource (PostgreSQL 16, 17+), Citus, a home region per tenant,
|
|
117
|
+
read replicas, tracing and SLOs, backups and point-in-time recovery, and committing a solver's
|
|
118
|
+
plan through Slotlock.
|
|
119
|
+
- Shared request budgets: `SLOTLOCK_SHARED_LIMITS=on` makes every server of a deployment draw a
|
|
120
|
+
tenant's `tenant_*` and `app_*` budgets from one token bucket in PostgreSQL
|
|
121
|
+
(`slotlock.take_rate_token`, keyed by a digest), after its own buckets pass; a database that
|
|
122
|
+
cannot be reached leaves each server's own budgets in charge. `createSlotlockRequestLimits` takes
|
|
123
|
+
`shared`. Run `slotlock migrate`.
|
|
124
|
+
- Feed health: `GET /v1/resources/{resource_id}/coverage` (`slotlock_get_coverage`, `listCoverage`)
|
|
125
|
+
shows each source's window, last read, last error and whether it is stale;
|
|
126
|
+
`POST /v1/resources/{resource_id}/coverage/{source}/failure` (`slotlock_record_coverage_failure`,
|
|
127
|
+
`recordCoverageFailure`) records a failed read. Maintenance puts a source that has gone stale on the
|
|
128
|
+
feed once (`coverage.stale`, `reportStaleCoverage`) and counts `staleSources` (`stale_sources`).
|
|
129
|
+
Unread time stays unproven, never free. Run `slotlock migrate`.
|
|
130
|
+
- Time zone rule changes: each calendar event keeps the wall time it was booked for in its zone,
|
|
131
|
+
and maintenance reports a future one-off event whose wall time now falls at another instant
|
|
132
|
+
(`event.timezone_shifted` on the feed, once), or with `followZoneRules` moves it there as a new
|
|
133
|
+
revision unless that overlaps (`event.timezone_conflict`). `store.followZoneRules` runs it alone.
|
|
134
|
+
Maintenance results gain `zoneShifts` (`zone_shifts` in the API); `/healthz` gains `tzdata`. Run
|
|
135
|
+
`slotlock migrate`.
|
|
136
|
+
- History: every change to a booking, hold or event records who made it (the API key or agent:
|
|
137
|
+
`withTenant`'s new `actor`, which the agent and application backends set to the principal's
|
|
138
|
+
subject) and, for a change of an existing record, what it was before. Read it newest first with
|
|
139
|
+
`GET /v1/resources/{resource_id}/history`, `GET /v1/reservations/{reservation_ref}/history` and
|
|
140
|
+
`GET /v1/tenant/events/{event_id}/history`, or `listHistory`. A backend of your own implements
|
|
141
|
+
`getHistory`. The feed and history now keep changes 30 days. Run `slotlock migrate`.
|
|
142
|
+
- Pools: interchangeable resources sharing a class (a vehicle class, a room type).
|
|
143
|
+
`PUT /v1/resources/{resource_id}/pool` (`slotlock_set_resource_pool`, `setResourcePool`) puts a
|
|
144
|
+
resource in one; `GET /v1/pools` lists them with their capacity. `slotlock_book_pool` (REST
|
|
145
|
+
`POST /v1/pool-bookings`) books the first member free and allowed in one transaction, so a class
|
|
146
|
+
sells out exactly at its size however many race (`pool_full`), and replays to the member it
|
|
147
|
+
booked. `slotlock_list_resources` shows each resource's `pool` and takes `pool` to list one's
|
|
148
|
+
members. Resources in the application API have `pool`. A backend of your own implements
|
|
149
|
+
`setResourcePool` and `listPools`, and the optional `bookPool`. Run `slotlock migrate`.
|
|
150
|
+
- Bookings across resources: `slotlock_book_together` (REST `POST /v1/bundles`) books the same
|
|
151
|
+
time on 2-20 resources in one transaction, all or none, and its refusal names the `resource_id`
|
|
152
|
+
that refused; bundles lock in one order, so racing ones never deadlock. It is a write that
|
|
153
|
+
`SLOTLOCK_CONFIRM_WRITES` can guard. `slotlock_find_available_slots` takes `require_free` for
|
|
154
|
+
times when every resource, or any n, is free together, each slot naming them in `resource_ids`.
|
|
155
|
+
A backend of your own implements the optional `bookTogether`, else `bundles_unavailable`.
|
|
156
|
+
- `SlotlockAgentOperationError` takes `details` (at most five short fields, such as
|
|
157
|
+
`resource_id`), which every protocol carries beside `code`.
|
|
158
|
+
- An event feed and webhooks. Every committed change to a tenant's holds, bookings and calendar
|
|
159
|
+
events is recorded in the same transaction by trigger: ids, times and states, never titles or
|
|
160
|
+
attendees. `GET /v1/changes` (`slotlock_list_changes`, `listChanges`) lists them as CloudEvents
|
|
161
|
+
by a cursor that no late-committing transaction can be skipped behind; changes are kept 30 days.
|
|
162
|
+
`POST /v1/webhooks` (`slotlock_create_webhook` and list, delete, enable) sends them in order,
|
|
163
|
+
signed as Standard Webhooks, at least once per `webhook-id`, retried with backoff for a day and
|
|
164
|
+
then disabled (`webhook.disabled` on the feed). `slotlock serve` delivers (`SLOTLOCK_WEBHOOKS`),
|
|
165
|
+
never to a private network address. Maintenance prunes the feed. A backend of your own
|
|
166
|
+
implements the five new `SlotlockApplicationBackend` methods. Run `slotlock migrate`.
|
|
167
|
+
- Booking rules per resource (`SlotlockBookingRules`): minimum notice, how far ahead, the step
|
|
168
|
+
starts fall on in the resource's time zone, a turnaround kept free after each booking (as its
|
|
169
|
+
trailing buffer, which the exclusion constraint enforces), and whether bookings must lie inside
|
|
170
|
+
its hours. Holds, reservations, moves and agents' events are refused with the rule they break
|
|
171
|
+
(`booking_rule_violated`; to an agent `booking_rule_min_notice`, `_max_advance`, `_slot_step`,
|
|
172
|
+
`_outside_hours`), by the database's clock; searches offer only what the rules allow. Set them
|
|
173
|
+
with `slotlock rules set`, `PUT /v1/resources/{resource_id}/booking-rules`
|
|
174
|
+
(`slotlock_set_resource_booking_rules`) or `setResourceBookingRules`. `putCalendarEvent` checks
|
|
175
|
+
them with `enforceBookingRules`; an imported event is not checked. `createSlotlockStore` takes
|
|
176
|
+
`defaultAvailabilityRules` for resources without hours of their own (`slotlock serve` passes
|
|
177
|
+
`SLOTLOCK_AVAILABILITY`). Run `slotlock migrate`.
|
|
178
|
+
- Agent holds: `slotlock_hold_slots` (REST `POST /v1/slot-holds`) holds up to five candidate slots
|
|
179
|
+
for 30-900 seconds as one group while a person chooses, listing the taken ones by index;
|
|
180
|
+
`slotlock_create_event`'s `hold_id` books one, releasing the others in the same transaction; and
|
|
181
|
+
`slotlock_release_slots` (`DELETE /v1/slot-holds/{hold_id}`) releases a group at once. A hold is
|
|
182
|
+
its agent's alone, and one agent may have 20 live (`SLOTLOCK_MAX_HOLDS_PER_HOLDER`). In the
|
|
183
|
+
library, `acquireHold` takes `holderRef` and `groupId`, `releaseHold` a `holderRef`, and
|
|
184
|
+
`putCalendarEvent` `fromHold` (refused with `hold_not_found` or `hold_mismatch`). A backend of
|
|
185
|
+
your own implements the optional `holdSlots` and `releaseSlots`; without them the tools answer
|
|
186
|
+
`holds_unavailable`. Run `slotlock migrate`. In an MCP Apps host the calendar view renders the
|
|
187
|
+
held slots, with when they expire, and its message names the hold, so the person picks from time
|
|
188
|
+
nobody can take while they decide.
|
|
189
|
+
- `slotlock_find_available_slots` (REST `GET /v1/available-slots`) lists up to 50 certain slots of
|
|
190
|
+
a duration, earliest first across up to 20 resources, each found the way
|
|
191
|
+
`slotlock_find_next_available` finds one: free of busy time, inside the resource's hours, and
|
|
192
|
+
only when every resource's coverage is certain.
|
|
193
|
+
- The MCP Apps calendar view lets a person pick one of those slots: a strip of days, each day's times
|
|
194
|
+
by part of the day, and *Book this time*. It sends the agent a chat message naming the slot
|
|
195
|
+
(`ui/message`), so the agent books it through the normal, confirmable path, and shows the text to
|
|
196
|
+
send when the host refuses.
|
|
197
|
+
- The view follows the host: its theme and its standard style variables, which it now applies (it
|
|
198
|
+
only read them before, so it looked the same, and light, in every host). It shows times in the
|
|
199
|
+
person's locale, clock and time zone, and reports its size to the host.
|
|
200
|
+
|
|
201
|
+
### Changed
|
|
202
|
+
|
|
203
|
+
- REST refusals are RFC 9457 problem details (`application/problem+json`): `type` (a tag URI, one per
|
|
204
|
+
code), `title`, `status` and `code`, with `error.code` kept so existing clients read them as
|
|
205
|
+
before, and a `Link: rel="describedby"` header to the error docs. An `overlap` names the
|
|
206
|
+
`resource_id`, `start` and `end` it refused.
|
|
207
|
+
- The same idempotency key with another request answers REST `422 idempotency_conflict` (was 409),
|
|
208
|
+
as draft-ietf-httpapi-idempotency-key-header-08 says. MCP and A2A answer the same code.
|
|
209
|
+
- Every REST write that keeps an `idempotency_key` (the agent tools' as well as the application's)
|
|
210
|
+
takes it as the `Idempotency-Key` header, as a Structured Field String (`"…"`) or bare; a write
|
|
211
|
+
that requires one and gets neither answers `400 idempotency_key_required`. Keys are kept 30 days
|
|
212
|
+
for event writes and while the hold could be live for holds; the docs publish the policy.
|
|
213
|
+
|
|
214
|
+
- The application API's resource has `booking_rules` (`null` when it has none) and `pool` (`null`
|
|
215
|
+
when it is in none).
|
|
216
|
+
- `SlotlockApplicationBackend` has twenty more methods, one per new application operation:
|
|
217
|
+
`setResourceBookingRules`, `setResourcePool`, `listPools`, `getResourceHistory`,
|
|
218
|
+
`getReservationHistory`, `getTenantEventHistory`, `getCoverage`, `recordCoverageFailure`,
|
|
219
|
+
`listChanges`, `createWebhook`, `listWebhooks`, `deleteWebhook`, `enableWebhook`,
|
|
220
|
+
`exportResourceAvailability`, `importResourceAvailability`, `exportTenantEvent`,
|
|
221
|
+
`checkInReservation`, `joinWaitlist`, `listWaitlist` and `leaveWaitlist`. A backend of your own
|
|
222
|
+
implements them, or wraps `createSlotlockStoreApplicationBackend`, which has them all. The agent
|
|
223
|
+
backend's new methods (`holdSlots`, `releaseSlots`, `bookTogether`, `bookPool`) are optional: on
|
|
224
|
+
a backend without one, its tool answers `409` with `holds_unavailable`, `bundles_unavailable` or
|
|
225
|
+
`pools_unavailable`.
|
|
226
|
+
- Maintenance results (`SlotlockMaintenanceResult`, and `POST /v1/maintenance`) have more counts:
|
|
227
|
+
`zoneShifts`, `staleSources`, `noShows` and `waitlistOffers` (`zone_shifts`, `stale_sources`,
|
|
228
|
+
`no_shows`, `waitlist_offers`).
|
|
229
|
+
- The three write tools carry `_meta.ui.visibility: ["model"]`: MCP Apps hosts no longer let a view
|
|
230
|
+
call them (the default is model and app).
|
|
231
|
+
|
|
232
|
+
### Fixed
|
|
233
|
+
|
|
234
|
+
- Retrying a recurring event's create (or any put) with the same idempotency key and arguments
|
|
235
|
+
after UTC midnight replays the first result again. The command's hash included the window its
|
|
236
|
+
occurrences are stored over, which for a series is the rolling horizon starting at the current
|
|
237
|
+
UTC midnight, so the next day's retry was refused as `idempotency_conflict`. A command stored by
|
|
238
|
+
0.3.0 still replays: a one-off event's always, a series' on the day it was stored.
|
|
239
|
+
- A hold's expiry is judged at each statement (`statement_timestamp()`), not at the start of the
|
|
240
|
+
transaction (`now()`). A store made from a caller's transaction runs in savepoints of it, so a hold
|
|
241
|
+
that expired while that transaction ran still looked live there and could be confirmed after its
|
|
242
|
+
expiry. Its time to live also starts when it is asked for, not when the caller's transaction did.
|
|
243
|
+
- An accepted write confirmation runs its write once. `slotlock serve` records each acceptance's
|
|
244
|
+
nonce in Postgres until it expires, on every server sharing the database, and refuses the same
|
|
245
|
+
acceptance sent again (`confirmation_invalid`); before, it could be sent again until it expired
|
|
246
|
+
(the write's idempotency key or revision then decided). `SlotlockAgentConfirmationOptions` takes
|
|
247
|
+
`consumeAcceptance` for this, and `createSlotlockAcceptanceRecords(sql)` implements it. Run
|
|
248
|
+
`slotlock migrate`: `serve` refuses to start with `SLOTLOCK_CONFIRM_WRITES` on a schema that cannot
|
|
249
|
+
record them.
|
|
250
|
+
- The production checklist's erasure item names `slotlock tenant erase` and `eraseTenant`, which
|
|
251
|
+
delete everything a tenant owns in one transaction, instead of a table-by-table delete.
|
|
252
|
+
|
|
253
|
+
- The OpenAPI document declares the `404` and `409` the agent-tool routes answer: `404` on
|
|
254
|
+
free/busy, next available and reading or listing events, and `404` and `409` (`overlap`, a stale
|
|
255
|
+
revision) on creating, changing and cancelling one. Clients generated from it now type an overlap
|
|
256
|
+
instead of falling to `default`.
|
|
257
|
+
|
|
258
|
+
## 0.3.0 - 2026-10-27
|
|
259
|
+
|
|
260
|
+
0.3.0 fixes the double bookings 0.2.0 could accept around daylight-saving changes and makes
|
|
261
|
+
`slotlock serve` safe to share between tenants. Its themes: a recurrence engine that keeps every
|
|
262
|
+
occurrence's whole time across clock changes; per-tenant limits, suspension and erasure in the
|
|
263
|
+
store; request limits and fair shares of the server in `slotlock serve`; the application API's
|
|
264
|
+
reads, resource lifecycle and tenant calls; a release path that checks what it publishes and
|
|
265
|
+
publishes the five language clients; and fixes for the findings of a security audit. Read the
|
|
266
|
+
upgrade notes first.
|
|
267
|
+
|
|
268
|
+
### Upgrade notes
|
|
269
|
+
|
|
270
|
+
- Run `slotlock migrate` with 0.3.0 before serving it. The schema now records its version, and
|
|
271
|
+
`slotlock serve` refuses a database an older release migrated (`schema_outdated`) or a newer one
|
|
272
|
+
did (`schema_newer`). A migration refuses a schema a newer release migrated, and PostgreSQL older
|
|
273
|
+
than 16. It also grants the serving role what this release needs: a role without that grant is
|
|
274
|
+
refused `application_role_not_granted`, so a library user runs `grantApplicationRole` again.
|
|
275
|
+
Each migration step waits at most a second for a lock and tries again, so serving queries queue
|
|
276
|
+
behind it for at most that long, and an unchanged schema takes no table lock.
|
|
277
|
+
- Before upgrading, run `storedCalendarEventProblem(event)` over your stored events. A series an
|
|
278
|
+
earlier release stored and this one refuses (a rule shape it no longer expands, `COUNT` with a
|
|
279
|
+
yearly day some years lack, or an exception that names no occurrence) keeps its old coverage and
|
|
280
|
+
is counted as refused at every roll.
|
|
281
|
+
- Breaking: `slotlock serve` defaults `SLOTLOCK_CONFIRM_WRITES` to `none`, so an authenticated
|
|
282
|
+
write takes effect at once. Set `all`, or a list of write tools, to keep confirmation;
|
|
283
|
+
`SLOTLOCK_CONFIRMATION_SECRET` is required only then.
|
|
284
|
+
- Breaking behind a proxy: `slotlock serve` now limits requests per client address, so set
|
|
285
|
+
`SLOTLOCK_TRUSTED_PROXY_HEADER` (`CF-Connecting-IP` behind Cloudflare, also when kamal-proxy or
|
|
286
|
+
another proxy sits between) before upgrading a server behind one. Without it every client counts
|
|
287
|
+
as the proxy's one address and shares one `address` budget, 50 requests a second by default.
|
|
288
|
+
- `slotlock serve` applies new limits by default, each set with `SLOTLOCK_LIMITS`: per tenant,
|
|
289
|
+
10,000 holds (`tenant_holds`) and 100,000 each of bookings, events and coverage rows
|
|
290
|
+
(`tenant_reservations`, `tenant_events`, `tenant_coverage`); 256 live-update streams on the
|
|
291
|
+
server (`subscriptions`), 16 a tenant (`tenant_subscriptions`) and 4 a key
|
|
292
|
+
(`key_subscriptions`); 2 of the pool's 10 connections a tenant (`tenant_transactions`; a server
|
|
293
|
+
with one tenant can set 10); one recurring write at a time per tenant and per server
|
|
294
|
+
(`tenant_series_writes`, `series_writes`) and one application maintenance run
|
|
295
|
+
(`maintenance_runs`); and request rates (Added). Check them against your largest tenant.
|
|
296
|
+
- Clients should handle the new answers: 429 `rate_limited` with `Retry-After`; 503
|
|
297
|
+
`server_busy` (a full key-lookup or series-write queue, or a transaction that waited more than 5
|
|
298
|
+
seconds for a lock) and 503 `request_aborted` (the request ended before its call did); 400
|
|
299
|
+
`calendar_work_limit` from free/busy, next-available and busy reads; 409
|
|
300
|
+
`hold_limit_reached`, `reservation_limit_reached`, `event_limit_reached`,
|
|
301
|
+
`coverage_limit_reached` and `resource_limit_reached`; and 403 `tenant_suspended`.
|
|
302
|
+
- Writes 0.2.0 accepted that are now refused: a control character other than tab or line break in
|
|
303
|
+
an event's title, description, location or participant names, or any in its UID
|
|
304
|
+
(`invalid_event`); event times outside years 1 to 9999; rule shapes ical.js expands differently
|
|
305
|
+
from RFC 5545 (`recurrenceRuleProblem(rule)` says why), and in a stored rule `COUNT=0`, an
|
|
306
|
+
unknown or repeated part or a sub-daily frequency; DTEND with DURATION; an exception that names
|
|
307
|
+
no occurrence; more than 8 RRULEs on a feed event; in the trusted importer, a DTEND the clocks
|
|
308
|
+
skip, a DURATION in days or weeks outside UTC and an all-day series whose first midnight is
|
|
309
|
+
skipped; and a booking, hold or event past the rolling horizon on a resource whose series goes on
|
|
310
|
+
past it (`recurrence_not_materialized`).
|
|
311
|
+
- Breaking for library callers: `findNextAvailableFor` requires `tenantRef` (`null` for a resource
|
|
312
|
+
with no tenant). `withTenant` opens its own transactions READ COMMITTED, and inside a caller's
|
|
313
|
+
REPEATABLE READ or SERIALIZABLE transaction refuses writes with `invalid_transaction_isolation`.
|
|
314
|
+
`createSlotlockApiKeyAuthenticator` throws `SlotlockAgentOperationError` `server_busy` (503) when
|
|
315
|
+
its lookup queue is full, instead of answering anonymous; `createSlotlockAgentServer` answers it
|
|
316
|
+
503 with `Retry-After` on REST, MCP and A2A. During maintenance a series whose roll fails is
|
|
317
|
+
reported in `refusedEventIds` and put back for an hour instead of failing the pass.
|
|
318
|
+
- Clients and OpenAPI: new optional arguments come after the old ones in every client, and the
|
|
319
|
+
.NET client keeps 0.2.0's parameter lists as overloads, so a call passing the cancellation token
|
|
320
|
+
by position still compiles. A Rust input struct written out field by field needs
|
|
321
|
+
`..Default::default()` for the fields it gained (`CreateResourceInput`, `AcquireHoldInput`,
|
|
322
|
+
`UpdateEventInput`), and a Go one written without field names needs them. Event participant
|
|
323
|
+
addresses in responses are no longer `format: email` in openapi.json; they follow the store's
|
|
324
|
+
own grammar (one `@`, no spaces), so a client that checked them as email addresses should stop.
|
|
325
|
+
- The dashboard Worker takes a new binding, `SLOTLOCK_SIGN_IN_CEILING_RATE_LIMIT`, for every
|
|
326
|
+
client together. Until it is bound, `SLOTLOCK_SIGN_IN_RATE_LIMIT` counts every client together,
|
|
327
|
+
as before, and the Worker logs a warning: add the ceiling before lowering that binding's limit
|
|
328
|
+
to one client's.
|
|
329
|
+
- The server image runs Node.js 24 LTS (24.22.1). 0.2.0's image ran Node.js 26.11.1.
|
|
330
|
+
|
|
331
|
+
### Security
|
|
332
|
+
|
|
333
|
+
- zod moves from 4.6.5 to 4.6.7 (#59), past GHSA-mrhx-c7xc-m3w5 (high): when validating an
|
|
334
|
+
oversized array, zod 4.6.5 collects an issue for every failing element with no cap, so one
|
|
335
|
+
authenticated tool or REST call could hold far more memory than its body. 0.1.0, 0.1.1 and 0.2.0
|
|
336
|
+
pin zod 4.6.5 exactly; their users can add `"overrides": {"zod": "4.6.7"}` to their own
|
|
337
|
+
`package.json` (README, Known issues in 0.2.0).
|
|
338
|
+
- `slotlock serve` had no request limits. It now limits requests per client address, key, tenant
|
|
339
|
+
and server (Added), and caps the work one tenant can start, store or hold open (Upgrade notes).
|
|
340
|
+
- A flood of made-up API keys no longer makes real keys answer 401 as if revoked. A full lookup
|
|
341
|
+
queue answers 503 `server_busy`, and keys that authenticated in the last 15 minutes wait in a
|
|
342
|
+
queue of their own, served first.
|
|
343
|
+
- The store's next-available search took no tenant and read no coverage: it offered slots on
|
|
344
|
+
another tenant's resource, on unknown ids and in a series' unstored time. It now answers
|
|
345
|
+
`resource_not_found` for those and offers a slot only where free/busy proves the time complete.
|
|
346
|
+
`acquireHold`, `createReservation` and `listBusy` take `tenantRef` and enforce it whatever the
|
|
347
|
+
connection's row-level security. An id the store never issues answers not found, not a raw error.
|
|
348
|
+
- One tenant can no longer hold every database connection of `slotlock serve`. 12 one-off creates
|
|
349
|
+
waiting on their tenant's lock held the shared pool of 10, and another tenant waited about a
|
|
350
|
+
second. A tenant's transactions now run at most 2 at once (`tenant_transactions`), and one that
|
|
351
|
+
waits more than 5 seconds for a lock answers 503 `server_busy`. `createSlotlockStore` takes
|
|
352
|
+
`maxTenantTransactions` and `tenantLockTimeoutMs` to do the same.
|
|
353
|
+
- `slotlock serve` bounds what one tenant may store. It gave its store no tenant limits, so an admin
|
|
354
|
+
key could add holds, bookings and coverage rows (and an agent events) without end. Every surface
|
|
355
|
+
now shares the store's tenant limits (Upgrade notes); a write past one answers 409 with its
|
|
356
|
+
`*_limit_reached` code.
|
|
357
|
+
- Expired holds now count against the hold limit until deleted, and keyed holds until they would
|
|
358
|
+
have expired, so one-millisecond holds cannot store rows without end between maintenance passes.
|
|
359
|
+
- Many tenants can no longer hold every live-update stream between them: 16 tenants within their
|
|
360
|
+
share could take all 256. When the server is full, a tenant holding fewer than the tenant holding
|
|
361
|
+
most, less one, ends that tenant's oldest stream gracefully (reported as `preempted`).
|
|
362
|
+
- One API key can no longer hold every live-update stream of its tenant: a key's cap
|
|
363
|
+
(`key_subscriptions`) is kept below its tenant's unless the tenant may hold only one.
|
|
364
|
+
- A key without the admin scope no longer spends its tenant's application API budget. Its refused
|
|
365
|
+
calls to application routes took tokens from `app_read` and `app_write`, so a read-only key could
|
|
366
|
+
keep the tenant's admin application answered 429.
|
|
367
|
+
- A burst of one tenant's recurring writes no longer holds the server from other tenants: such
|
|
368
|
+
writes wait for slots shared between tenants (Added). In the audit's test another tenant's
|
|
369
|
+
worst read went from 12.4 s to about 0.3 s.
|
|
370
|
+
- `slotlock_get_free_busy` and `slotlock_find_next_available` read a resource named more than once
|
|
371
|
+
once, and read 10,000 busy intervals in all across a request, answering `400
|
|
372
|
+
calendar_work_limit` past that. A request naming 100 resources, or one resource 100 times, did
|
|
373
|
+
100 times the work of one.
|
|
374
|
+
- `slotlock_find_next_available` expands 100,000 bookable windows in all, answering `400
|
|
375
|
+
calendar_work_limit` past that, and walks a resource's busy time once rather than once per
|
|
376
|
+
window. 100 resources with 50 daily rules each, searched over a year, held the process for about
|
|
377
|
+
seven seconds.
|
|
378
|
+
- `slotlock_list_events` reads a resource named more than once once. One id named 100 times read
|
|
379
|
+
its events 100 times and returned the first event 100 times.
|
|
380
|
+
- The application API's busy read (`GET /v1/resources/{id}/busy`) stops at the first interval past
|
|
381
|
+
the 10,000 it may return, instead of reading the whole window first. `store.listBusy` takes a
|
|
382
|
+
`limit`.
|
|
383
|
+
- The recurrence budget counts work, not time, and now charges what each step costs, time zone
|
|
384
|
+
reads included. Fifty writes of `FREQ=DAILY;INTERVAL=9999;BYMONTH=2;BYMONTHDAY=29` held the
|
|
385
|
+
server for 23 seconds, and forty daily London series held a feed for 35 seconds; a refused write
|
|
386
|
+
now ends within about a tenth of a second, and a feed within about half a second.
|
|
387
|
+
- A feed event may list 8 RRULEs, and each one past the first costs a step at every occurrence it
|
|
388
|
+
passes. One event with 110,000 RRULEs (2 MB) held `parseICalendarFeed` for about 9 seconds.
|
|
389
|
+
- One recurring series can no longer stop its tenant's maintenance. A rule naming two wall times in
|
|
390
|
+
a spring-forward gap (`FREQ=DAILY;BYHOUR=2,3;BYMINUTE=0,30` in New York) gave one instant twice,
|
|
391
|
+
its roll failed every pass, and no other series of the tenant extended or had its expired holds
|
|
392
|
+
purged. An instant is now one occurrence wherever the rule names it again, and a series whose
|
|
393
|
+
roll fails for any other reason is refused and put back (Upgrade notes).
|
|
394
|
+
- An event's title, description, location and participant names may hold a tab or a line break,
|
|
395
|
+
and no other control character; its UID none. ical.js left a bare CR in what `emitICalendar` and
|
|
396
|
+
`emitITipCalendar` wrote, so a reader that ends lines at one read the rest as properties of its
|
|
397
|
+
own (`METHOD:CANCEL`). A CRLF line break is kept as LF.
|
|
398
|
+
- The Node listener refuses a request target with a dot segment between backslashes
|
|
399
|
+
(`/v1/resources\..\availability`), as it refuses one between slashes: URL parsing reads a
|
|
400
|
+
backslash as a slash. An encoded backslash (`%5C`) is still an ordinary character.
|
|
401
|
+
- A write confirmation no longer states a series' dates in UTC when it could not read the event (a
|
|
402
|
+
write-only key, a refused or slow read) and the update names no time zone. The store books the
|
|
403
|
+
series in the event's own zone, so the dates approved could differ from those booked. The
|
|
404
|
+
question now shows the rule and says its dates follow the event's existing zone.
|
|
405
|
+
- An event whose organizer or attendee address the store keeps but `z.email()` refuses, such as
|
|
406
|
+
`ops@localhost` from an imported feed, no longer makes every listing of its window answer 500 and
|
|
407
|
+
stop paginating (Upgrade notes).
|
|
408
|
+
- Dashboard sign-ins are counted per client network, in the Worker and in `slotlock serve`: an
|
|
409
|
+
IPv4 address, or an IPv6 client's /48, so a site rotating through its addresses cannot use up the
|
|
410
|
+
ceiling every other client shares. Every other request counts an IPv6 client as its /64. The
|
|
411
|
+
dashboard runs at most two sign-ins from one client at once.
|
|
412
|
+
- The example dashboard Worker configuration removes query strings, which hold OAuth codes, from
|
|
413
|
+
the URLs Workers Logs keep.
|
|
414
|
+
- SECURITY.md and the README say write confirmation is attested by the client: it keeps an agent
|
|
415
|
+
from writing unasked and is not an authorization boundary. SECURITY.md also says how hosted
|
|
416
|
+
Slotlock may be tested: your own account only, no other account's data, no load or denial of
|
|
417
|
+
service, reports through private vulnerability reporting.
|
|
418
|
+
- Development lockfiles only (nothing reaches the package or the image): the site's and the
|
|
419
|
+
dashboard Worker's Wrangler move undici from 7.29.1 to 7.30.1 (GHSA-xjj6-f33q-929c, high), and
|
|
420
|
+
the development lockfile moves body-parser from 2.3.0 to 2.4.0 (GHSA-27r3-jxmh-p828).
|
|
421
|
+
|
|
422
|
+
### Added
|
|
423
|
+
|
|
424
|
+
- The application API serves the store calls it lacked, each in `openapi.json` and the five
|
|
425
|
+
clients: a resource's `visibility` (`private` ones are never shown to agents) and `archived_at`;
|
|
426
|
+
`POST /v1/resources/{id}/archive` and `…/restore`, and `DELETE /v1/resources/{id}` (`409
|
|
427
|
+
resource_in_use` while anything on it is live); `GET /v1/holds/{id}`,
|
|
428
|
+
`GET /v1/resources/{id}/holds`, `GET /v1/reservations/{ref}`,
|
|
429
|
+
`GET /v1/resources/{id}/reservations` and `GET /v1/resources/{id}/busy`, paged and bounded to
|
|
430
|
+
367 days; `GET /v1/tenant/events` and `GET`/`DELETE /v1/tenant/events/{id}`, which reach and
|
|
431
|
+
cancel any key's event, a revoked one's included; and `GET /v1/tenant`, the key's tenant and
|
|
432
|
+
whether it is suspended.
|
|
433
|
+
- `POST /v1/tenant/erase` erases the key's tenant, with its reference as `confirm_tenant_ref`. It is
|
|
434
|
+
off unless the application backend is created with `tenantErasure: true` (`slotlock serve` leaves
|
|
435
|
+
it off) and refused while the tenant is suspended.
|
|
436
|
+
- `POST /v1/holds` and `DELETE /v1/tenant/events/{id}` take a retry identity as the
|
|
437
|
+
`Idempotency-Key` header as well as `idempotency_key`; a hold retried with it answers the first
|
|
438
|
+
hold, and the key reused with another request is `409 idempotency_conflict`. Another application
|
|
439
|
+
route refuses the header (`400 idempotency_key_not_supported`) rather than ignore it.
|
|
440
|
+
- `POST /v1/holds` takes `buffer_after_minutes` (0 to 30 days), as a booking does: the time after
|
|
441
|
+
the hold stays busy, and the booking it is confirmed as keeps it.
|
|
442
|
+
- In the store: private resources, which agents neither list, describe nor book; `archiveResource`,
|
|
443
|
+
`restoreResource` and `deleteResource`; holds with a turnaround buffer the booking keeps and a
|
|
444
|
+
retry identity; `getHold`, `listHolds`, `getReservation` and `listReservations`; and reading,
|
|
445
|
+
listing and cancelling any key's event as the tenant.
|
|
446
|
+
- Tenant limits in the store (`tenantLimits`: resources, holds, reservations, events, coverage),
|
|
447
|
+
refused with `*_limit_reached`. They count what is live or still to come plus what the tenant
|
|
448
|
+
added in the last 30 days, so ended history leaves them. Maintenance now deletes expired holds.
|
|
449
|
+
- Tenant suspension and erasure: the deployment role may `suspendTenant` (with a reason) and
|
|
450
|
+
`unsuspendTenant`; a suspended tenant may read, its writes are refused `tenant_suspended`, and it
|
|
451
|
+
gets no new API key. `eraseTenant` deletes every row a tenant owns in one call and says how many
|
|
452
|
+
from each table. No serving role may suspend.
|
|
453
|
+
- `slotlock resource archive|restore|delete <ref>` and `slotlock tenant status|suspend|unsuspend|erase`.
|
|
454
|
+
`suspend` and `unsuspend` connect as `SLOTLOCK_MIGRATE_DATABASE_URL`'s role, the only one that
|
|
455
|
+
may; `erase` needs `--confirm` repeating `SLOTLOCK_TENANT`.
|
|
456
|
+
- `slotlock_update_event` takes `expected_resource_id`: an event since moved to another resource is
|
|
457
|
+
not found, checked under the event's lock.
|
|
458
|
+
- `slotlock serve` limits requests: per client address before any key is looked up, then per API
|
|
459
|
+
key, per tenant and for the whole server, with reads and writes counted apart (by default 20
|
|
460
|
+
reads and 5 writes a second per key and per tenant). The application API counts in its tenant's
|
|
461
|
+
own budget (`app_read`, `app_write`). A refusal answers 429 `rate_limited` with `Retry-After` and
|
|
462
|
+
the `RateLimit` and `RateLimit-Policy` fields. `SLOTLOCK_LIMITS` changes the limits. Behind a
|
|
463
|
+
proxy without `SLOTLOCK_TRUSTED_PROXY_HEADER`, the first request carrying a client-address field
|
|
464
|
+
logs a `client_address_field_ignored` warning. The health check is not counted, and reuses one
|
|
465
|
+
database check a second.
|
|
466
|
+
- `slotlock serve` runs a recurring create and every event update one at a time per tenant and per
|
|
467
|
+
server (`tenant_series_writes`, `series_writes`), taking the write's token before it waits; each
|
|
468
|
+
server process counts its own slots. A freed slot rests for as long as the server was busy while
|
|
469
|
+
the write before held it, up to a second, so writes that each hold it under a second keep the
|
|
470
|
+
server busy at most about half the time. It then goes to the waiting tenant with the fewest
|
|
471
|
+
writes in flight and, among those, the one whose writes have kept the server busy least lately.
|
|
472
|
+
Tenants with more writes waiting never go ahead of one sending a write at a time, but many
|
|
473
|
+
tenants with one write each and less use, new ones included, can each go ahead of it once. Four
|
|
474
|
+
more of a tenant's writes may wait and the rest get 429. When the server's 64 places are taken,
|
|
475
|
+
a write from a tenant with fewer writes in flight than the one with the most takes the place of
|
|
476
|
+
that tenant's last write, which gets 503 `server_busy`. One that has waited two seconds gets 429
|
|
477
|
+
while its tenant's own write is still running, otherwise 503 `server_busy`. An application API
|
|
478
|
+
maintenance run waits the same way for slots of its own: one per tenant, `maintenance_runs` (1)
|
|
479
|
+
per server.
|
|
480
|
+
- `createSlotlockAgentServer` takes `subscriptions.maxPerTenant`, which `slotlock serve` sets from
|
|
481
|
+
`tenant_subscriptions`.
|
|
482
|
+
- `slotlock serve` logs a `refusals` line each minute there are any, and at shutdown: auth
|
|
483
|
+
refusals, rate limits, lookup overflows, busy series writes, busy maintenance runs and bodies too
|
|
484
|
+
large, with clients named only by per-process pseudonyms.
|
|
485
|
+
- `createSlotlockNodeServer` hands the fetch target the connection's remote address as a second
|
|
486
|
+
argument.
|
|
487
|
+
- `parseICalendarFeed` reads a feed series by series and names the series it refuses, with why,
|
|
488
|
+
instead of refusing the whole calendar; the worker pool offers it too.
|
|
489
|
+
`recurrenceRuleProblem(rule)` says why a rule shape is refused, and
|
|
490
|
+
`storedCalendarEventProblem(event)` why a stored event would be.
|
|
491
|
+
- An exported calendar carries a VTIMEZONE for each IANA zone it names, so a reader that does not
|
|
492
|
+
know the name reads the times as Slotlock does.
|
|
493
|
+
- The npm package ships `openapi.json`, the REST API's OpenAPI 3.1 document, exported as
|
|
494
|
+
`slotlock/openapi.json`. The capability manifest names the 0.2 surfaces: `rest-api`,
|
|
495
|
+
`application-api`, `api-keys`, `dashboard` and `maintenance`.
|
|
496
|
+
- The five language clients are published with each release: `slotlock` on PyPI and crates.io,
|
|
497
|
+
`Slotlock.Client` on NuGet, `io.pylota:slotlock` on Maven Central and the Go module's
|
|
498
|
+
`clients/go/v<version>` tag, each after the release suite passes and a maintainer approves. They
|
|
499
|
+
now carry the package's version (they said 0.1.0 beside a 0.2.0 server).
|
|
500
|
+
`docs/release/clients.md` has each registry's one-time setup.
|
|
501
|
+
- `scripts/check-stage.sh <stage-id>`, which a maintainer runs before `npm stage approve`. It
|
|
502
|
+
refuses a stage whose tarball is not the one the release tested, whose dist-tag would move
|
|
503
|
+
`latest` backwards, or whose runtime dependencies, installed as a consumer would that day, have a
|
|
504
|
+
high or critical advisory. 0.2.0 went public with zod 4.6.5 after `main` had moved past it.
|
|
505
|
+
- A known-issues notice for 0.2.0: in the README, on the site (`/docs/known-issues/`) and as text
|
|
506
|
+
for the v0.2.0 release notes, a pinned issue and two security advisories
|
|
507
|
+
(`docs/release/0.2.0-known-issues.md`). It names the six ways 0.2.0 can accept a double booking
|
|
508
|
+
around daylight-saving changes and series ends, and the zod pin with its workaround.
|
|
509
|
+
`docs/release/deprecations.md` gives the `npm deprecate` and `mcp-publisher status` commands for
|
|
510
|
+
0.0.0-stage, 0.1.0, 0.1.1 and 0.2.0.
|
|
511
|
+
- For hosted Slotlock: drafts of a privacy notice (`PRIVACY.md`), Terms of Service (`TERMS.md`,
|
|
512
|
+
with data processing terms) and an Acceptable Use Policy (`ACCEPTABLE-USE.md`), each marked for
|
|
513
|
+
owner and legal review; a service page (`docs/hosted/README.md`) with addresses, price, limits
|
|
514
|
+
and contacts; `VERSIONING.md`, what `/v1`, the tools and the version numbers promise; a public
|
|
515
|
+
status from an off-server probe (`docs/hosted/status.md`, off until the owner sets
|
|
516
|
+
`SLOTLOCK_STATUS_PROBE`); and `.github/SUPPORT.md`, the route for each kind of request.
|
|
517
|
+
|
|
518
|
+
### Changed
|
|
519
|
+
|
|
520
|
+
- A recurring event occupies both readings of each occurrence across a clock change: RFC 5545's
|
|
521
|
+
exact duration from its start, and its written end on the wall clock. A start in a skipped hour
|
|
522
|
+
moves forward by the gap, and one in a repeated hour takes the first instant. One-off events,
|
|
523
|
+
overrides and PERIOD RDATEs keep their exact times. Feeds, the trusted importer and the store all
|
|
524
|
+
read events this way.
|
|
525
|
+
- A stored series is expanded from the instants it holds, and its exceptions name occurrences by
|
|
526
|
+
UTC instant (exported as UTC `RECURRENCE-ID`s).
|
|
527
|
+
- A series' occurrences never overlap each other: one that reaches past the next one's start ends
|
|
528
|
+
there, and the next ends at the later of the two ends.
|
|
529
|
+
- Recurring series are stored for up to 369 days (`SLOTLOCK_MATERIALIZATION_WINDOW_DAYS`), so a
|
|
530
|
+
full 367-day search can be complete; search, free/busy and coverage windows keep their 367-day
|
|
531
|
+
cap.
|
|
532
|
+
- A booking, hold or event first extends each series on its resource that is not stored as far as
|
|
533
|
+
it reaches, so it meets their occurrences. A write past the rolling horizon on a resource whose
|
|
534
|
+
series goes on past it is refused with `recurrence_not_materialized`; a series known to have
|
|
535
|
+
ended does not count.
|
|
536
|
+
- A confirmed hold becomes a booking of its tenant whose reference is the hold's id, at revision 1,
|
|
537
|
+
so it can be read, moved and cancelled like any booking (`slotlock_confirm_hold` says so).
|
|
538
|
+
`createReservation` on a tenant's resource does the same. The migration, and maintenance for
|
|
539
|
+
rows a 0.2 server writes during a rolling deploy, give older such bookings their tenant.
|
|
540
|
+
- `server.json` (the MCP Registry entry) no longer marks `SLOTLOCK_AUTH_TOKEN` and
|
|
541
|
+
`SLOTLOCK_CONFIRMATION_SECRET` as required, and its Authorization header takes the token or an
|
|
542
|
+
API key.
|
|
543
|
+
- `slotlock --help` describes the server token as read and write, never admin.
|
|
544
|
+
- Releases: npm's `latest` dist-tag, and the image's `latest` tag, never move backwards. A release
|
|
545
|
+
on an older line takes `latest-<major>.<minor>` and a prerelease `next`, and a release stops when
|
|
546
|
+
the registry cannot say which version is `latest`. The image and the MCP Registry entry are
|
|
547
|
+
published only after the release's own suite passes, each behind an approval; the image's
|
|
548
|
+
attestation is still signed by `image.yml`. The packed NOTICE is written at build time from the
|
|
549
|
+
lockfile, so a dependency bump no longer breaks the release checks. **Release a version** takes
|
|
550
|
+
`dry_run`.
|
|
551
|
+
- Repository: CI runs the database suites on PostgreSQL 16, 17 and 18 and the Compose quickstart
|
|
552
|
+
end to end, and one check, "CI passed", gathers every job, the language clients included.
|
|
553
|
+
`scripts/configure-github.sh` requires it and DCO's "Signed-off-by" on `main` with no admin
|
|
554
|
+
bypass, puts the person who runs it as reviewer on the publishing environments, and sets up
|
|
555
|
+
CodeQL and Dependabot for every manifest.
|
|
556
|
+
|
|
557
|
+
### Fixed
|
|
558
|
+
|
|
559
|
+
- 0.2.0's six double-booking cases (Known issues in the README): a first occurrence spanning a
|
|
560
|
+
clock change, the repeated hour of an autumn change in stored series, exceptions in a skipped
|
|
561
|
+
hour, occurrences starting in a skipped hour, a timed series with a date-only `UNTIL` (now its
|
|
562
|
+
whole day on the series' clock), and an iCalendar `DURATION` counting hours across a spring
|
|
563
|
+
change.
|
|
564
|
+
- A weekly-hours window whose edge falls in a skipped hour stands where the gap ends, so it holds
|
|
565
|
+
only the wall times it names that exist. It used to move, or give nothing.
|
|
566
|
+
- A yearly rule on a day some years lack (29 February) occurs in the years that have it instead of
|
|
567
|
+
being refused.
|
|
568
|
+
- A feed's daily or weekly series from years ago is searched from near the window, so a daily
|
|
569
|
+
standup since 2014 no longer refuses its feed. A CalDAV server's expanded series whose first
|
|
570
|
+
instance has no `RECURRENCE-ID` keeps its later instances busy.
|
|
571
|
+
- A series whose DTEND falls in a skipped hour no longer holds an extra hour every later day.
|
|
572
|
+
- Series the roll cannot extend (in conflict or over the budget) no longer keep a healthy series
|
|
573
|
+
from rolling, however many there are. The roll puts each behind for an hour and takes those it
|
|
574
|
+
has never put back first; its cursor carries that place (`rollRetryAt`), and a pass says when it
|
|
575
|
+
stopped before them (`rollPending`), so `slotlock serve` and `slotlock maintain` go round again
|
|
576
|
+
at once. The roll names the series it could not extend because of a collision
|
|
577
|
+
(`conflictEventIds`).
|
|
578
|
+
- A refused update of a recurring event names the booking in its way, not its own first
|
|
579
|
+
occurrence.
|
|
580
|
+
- The quickstart: `.env.example` turns the REST API on, as `slotlock serve` and Compose do, and
|
|
581
|
+
the docs say PostgreSQL 16 or newer (Compose ships 18).
|
|
582
|
+
- Documentation: SPEC.md and the README describe 0.2 and later, and SPEC.md gains a daylight-saving
|
|
583
|
+
section; the README says which spelling of an availability rule each surface takes
|
|
584
|
+
(`startMinutes` for `slotlock hours set` and `SLOTLOCK_AVAILABILITY`, `start_minutes` over
|
|
585
|
+
REST); the site names the version to install and has pages for the clients, operations and the
|
|
586
|
+
hosted service.
|
|
587
|
+
|
|
6
588
|
## 0.2.0 - 2026-10-23
|
|
7
589
|
|
|
8
590
|
### Added
|