slotlock 0.1.1 → 0.3.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 (56) hide show
  1. package/CHANGELOG.md +455 -0
  2. package/CONTRIBUTING.md +55 -19
  3. package/GOVERNANCE.md +4 -2
  4. package/MAINTAINERS.md +3 -2
  5. package/NOTICE +4 -4
  6. package/README.md +403 -103
  7. package/SECURITY.md +70 -5
  8. package/SPEC.md +161 -21
  9. package/agent-server.d.ts +76 -10
  10. package/agent-server.js +381 -146
  11. package/agent-store-backend.d.ts +22 -0
  12. package/agent-store-backend.js +598 -24
  13. package/api-keys.d.ts +22 -7
  14. package/api-keys.js +38 -8
  15. package/application-api.d.ts +67 -0
  16. package/application-api.js +552 -0
  17. package/config.d.ts +93 -0
  18. package/config.js +192 -0
  19. package/dashboard.d.ts +10 -1
  20. package/dashboard.js +23 -5
  21. package/ddl.d.ts +47 -4
  22. package/ddl.js +679 -25
  23. package/engine.js +20 -6
  24. package/event-schema.d.ts +59 -0
  25. package/event-schema.js +85 -0
  26. package/hooks.d.ts +7 -0
  27. package/hooks.js +13 -0
  28. package/ical-worker-thread.js +4 -2
  29. package/ical-worker.d.ts +8 -5
  30. package/ical-worker.js +9 -2
  31. package/index.d.ts +12 -8
  32. package/index.js +7 -5
  33. package/maintenance.d.ts +67 -0
  34. package/maintenance.js +213 -0
  35. package/node-server.d.ts +9 -1
  36. package/node-server.js +127 -6
  37. package/openapi.json +6024 -0
  38. package/package.json +3 -2
  39. package/rate-limit.d.ts +71 -0
  40. package/rate-limit.js +718 -0
  41. package/rest-api.d.ts +33 -18
  42. package/rest-api.js +141 -63
  43. package/rules.d.ts +19 -0
  44. package/rules.js +38 -7
  45. package/self-host.d.ts +26 -9
  46. package/self-host.js +427 -47
  47. package/slotlock-manifest.schema.json +6 -1
  48. package/slotlock.manifest.json +7 -2
  49. package/store.d.ts +359 -27
  50. package/store.js +2077 -296
  51. package/sync.d.ts +43 -2
  52. package/sync.js +1950 -277
  53. package/timezone.d.ts +77 -3
  54. package/timezone.js +265 -21
  55. package/types.d.ts +217 -10
  56. package/weekly-hours.js +2 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,461 @@
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.3.0 - 2026-10-27
7
+
8
+ 0.3.0 fixes the double bookings 0.2.0 could accept around daylight-saving changes and makes
9
+ `slotlock serve` safe to share between tenants. Its themes: a recurrence engine that keeps every
10
+ occurrence's whole time across clock changes; per-tenant limits, suspension and erasure in the
11
+ store; request limits and fair shares of the server in `slotlock serve`; the application API's
12
+ reads, resource lifecycle and tenant calls; a release path that checks what it publishes and
13
+ publishes the five language clients; and fixes for the findings of a security audit. Read the
14
+ upgrade notes first.
15
+
16
+ ### Upgrade notes
17
+
18
+ - Run `slotlock migrate` with 0.3.0 before serving it. The schema now records its version, and
19
+ `slotlock serve` refuses a database an older release migrated (`schema_outdated`) or a newer one
20
+ did (`schema_newer`). A migration refuses a schema a newer release migrated, and PostgreSQL older
21
+ than 16. It also grants the serving role what this release needs: a role without that grant is
22
+ refused `application_role_not_granted`, so a library user runs `grantApplicationRole` again.
23
+ Each migration step waits at most a second for a lock and tries again, so serving queries queue
24
+ behind it for at most that long, and an unchanged schema takes no table lock.
25
+ - Before upgrading, run `storedCalendarEventProblem(event)` over your stored events. A series an
26
+ earlier release stored and this one refuses (a rule shape it no longer expands, `COUNT` with a
27
+ yearly day some years lack, or an exception that names no occurrence) keeps its old coverage and
28
+ is counted as refused at every roll.
29
+ - Breaking: `slotlock serve` defaults `SLOTLOCK_CONFIRM_WRITES` to `none`, so an authenticated
30
+ write takes effect at once. Set `all`, or a list of write tools, to keep confirmation;
31
+ `SLOTLOCK_CONFIRMATION_SECRET` is required only then.
32
+ - Breaking behind a proxy: `slotlock serve` now limits requests per client address, so set
33
+ `SLOTLOCK_TRUSTED_PROXY_HEADER` (`CF-Connecting-IP` behind Cloudflare, also when kamal-proxy or
34
+ another proxy sits between) before upgrading a server behind one. Without it every client counts
35
+ as the proxy's one address and shares one `address` budget, 50 requests a second by default.
36
+ - `slotlock serve` applies new limits by default, each set with `SLOTLOCK_LIMITS`: per tenant,
37
+ 10,000 holds (`tenant_holds`) and 100,000 each of bookings, events and coverage rows
38
+ (`tenant_reservations`, `tenant_events`, `tenant_coverage`); 256 live-update streams on the
39
+ server (`subscriptions`), 16 a tenant (`tenant_subscriptions`) and 4 a key
40
+ (`key_subscriptions`); 2 of the pool's 10 connections a tenant (`tenant_transactions`; a server
41
+ with one tenant can set 10); one recurring write at a time per tenant and per server
42
+ (`tenant_series_writes`, `series_writes`) and one application maintenance run
43
+ (`maintenance_runs`); and request rates (Added). Check them against your largest tenant.
44
+ - Clients should handle the new answers: 429 `rate_limited` with `Retry-After`; 503
45
+ `server_busy` (a full key-lookup or series-write queue, or a transaction that waited more than 5
46
+ seconds for a lock) and 503 `request_aborted` (the request ended before its call did); 400
47
+ `calendar_work_limit` from free/busy, next-available and busy reads; 409
48
+ `hold_limit_reached`, `reservation_limit_reached`, `event_limit_reached`,
49
+ `coverage_limit_reached` and `resource_limit_reached`; and 403 `tenant_suspended`.
50
+ - Writes 0.2.0 accepted that are now refused: a control character other than tab or line break in
51
+ an event's title, description, location or participant names, or any in its UID
52
+ (`invalid_event`); event times outside years 1 to 9999; rule shapes ical.js expands differently
53
+ from RFC 5545 (`recurrenceRuleProblem(rule)` says why), and in a stored rule `COUNT=0`, an
54
+ unknown or repeated part or a sub-daily frequency; DTEND with DURATION; an exception that names
55
+ no occurrence; more than 8 RRULEs on a feed event; in the trusted importer, a DTEND the clocks
56
+ skip, a DURATION in days or weeks outside UTC and an all-day series whose first midnight is
57
+ skipped; and a booking, hold or event past the rolling horizon on a resource whose series goes on
58
+ past it (`recurrence_not_materialized`).
59
+ - Breaking for library callers: `findNextAvailableFor` requires `tenantRef` (`null` for a resource
60
+ with no tenant). `withTenant` opens its own transactions READ COMMITTED, and inside a caller's
61
+ REPEATABLE READ or SERIALIZABLE transaction refuses writes with `invalid_transaction_isolation`.
62
+ `createSlotlockApiKeyAuthenticator` throws `SlotlockAgentOperationError` `server_busy` (503) when
63
+ its lookup queue is full, instead of answering anonymous; `createSlotlockAgentServer` answers it
64
+ 503 with `Retry-After` on REST, MCP and A2A. During maintenance a series whose roll fails is
65
+ reported in `refusedEventIds` and put back for an hour instead of failing the pass.
66
+ - Clients and OpenAPI: new optional arguments come after the old ones in every client, and the
67
+ .NET client keeps 0.2.0's parameter lists as overloads, so a call passing the cancellation token
68
+ by position still compiles. A Rust input struct written out field by field needs
69
+ `..Default::default()` for the fields it gained (`CreateResourceInput`, `AcquireHoldInput`,
70
+ `UpdateEventInput`), and a Go one written without field names needs them. Event participant
71
+ addresses in responses are no longer `format: email` in openapi.json; they follow the store's
72
+ own grammar (one `@`, no spaces), so a client that checked them as email addresses should stop.
73
+ - The dashboard Worker takes a new binding, `SLOTLOCK_SIGN_IN_CEILING_RATE_LIMIT`, for every
74
+ client together. Until it is bound, `SLOTLOCK_SIGN_IN_RATE_LIMIT` counts every client together,
75
+ as before, and the Worker logs a warning: add the ceiling before lowering that binding's limit
76
+ to one client's.
77
+ - The server image runs Node.js 24 LTS (24.22.1). 0.2.0's image ran Node.js 26.11.1.
78
+
79
+ ### Security
80
+
81
+ - zod moves from 4.6.5 to 4.6.7 (#59), past GHSA-mrhx-c7xc-m3w5 (high): when validating an
82
+ oversized array, zod 4.6.5 collects an issue for every failing element with no cap, so one
83
+ authenticated tool or REST call could hold far more memory than its body. 0.1.0, 0.1.1 and 0.2.0
84
+ pin zod 4.6.5 exactly; their users can add `"overrides": {"zod": "4.6.7"}` to their own
85
+ `package.json` (README, Known issues in 0.2.0).
86
+ - `slotlock serve` had no request limits. It now limits requests per client address, key, tenant
87
+ and server (Added), and caps the work one tenant can start, store or hold open (Upgrade notes).
88
+ - A flood of made-up API keys no longer makes real keys answer 401 as if revoked. A full lookup
89
+ queue answers 503 `server_busy`, and keys that authenticated in the last 15 minutes wait in a
90
+ queue of their own, served first.
91
+ - The store's next-available search took no tenant and read no coverage: it offered slots on
92
+ another tenant's resource, on unknown ids and in a series' unstored time. It now answers
93
+ `resource_not_found` for those and offers a slot only where free/busy proves the time complete.
94
+ `acquireHold`, `createReservation` and `listBusy` take `tenantRef` and enforce it whatever the
95
+ connection's row-level security. An id the store never issues answers not found, not a raw error.
96
+ - One tenant can no longer hold every database connection of `slotlock serve`. 12 one-off creates
97
+ waiting on their tenant's lock held the shared pool of 10, and another tenant waited about a
98
+ second. A tenant's transactions now run at most 2 at once (`tenant_transactions`), and one that
99
+ waits more than 5 seconds for a lock answers 503 `server_busy`. `createSlotlockStore` takes
100
+ `maxTenantTransactions` and `tenantLockTimeoutMs` to do the same.
101
+ - `slotlock serve` bounds what one tenant may store. It gave its store no tenant limits, so an admin
102
+ key could add holds, bookings and coverage rows (and an agent events) without end. Every surface
103
+ now shares the store's tenant limits (Upgrade notes); a write past one answers 409 with its
104
+ `*_limit_reached` code.
105
+ - Expired holds now count against the hold limit until deleted, and keyed holds until they would
106
+ have expired, so one-millisecond holds cannot store rows without end between maintenance passes.
107
+ - Many tenants can no longer hold every live-update stream between them: 16 tenants within their
108
+ share could take all 256. When the server is full, a tenant holding fewer than the tenant holding
109
+ most, less one, ends that tenant's oldest stream gracefully (reported as `preempted`).
110
+ - One API key can no longer hold every live-update stream of its tenant: a key's cap
111
+ (`key_subscriptions`) is kept below its tenant's unless the tenant may hold only one.
112
+ - A key without the admin scope no longer spends its tenant's application API budget. Its refused
113
+ calls to application routes took tokens from `app_read` and `app_write`, so a read-only key could
114
+ keep the tenant's admin application answered 429.
115
+ - A burst of one tenant's recurring writes no longer holds the server from other tenants: such
116
+ writes wait for slots shared between tenants (Added). In the audit's test another tenant's
117
+ worst read went from 12.4 s to about 0.3 s.
118
+ - `slotlock_get_free_busy` and `slotlock_find_next_available` read a resource named more than once
119
+ once, and read 10,000 busy intervals in all across a request, answering `400
120
+ calendar_work_limit` past that. A request naming 100 resources, or one resource 100 times, did
121
+ 100 times the work of one.
122
+ - `slotlock_find_next_available` expands 100,000 bookable windows in all, answering `400
123
+ calendar_work_limit` past that, and walks a resource's busy time once rather than once per
124
+ window. 100 resources with 50 daily rules each, searched over a year, held the process for about
125
+ seven seconds.
126
+ - `slotlock_list_events` reads a resource named more than once once. One id named 100 times read
127
+ its events 100 times and returned the first event 100 times.
128
+ - The application API's busy read (`GET /v1/resources/{id}/busy`) stops at the first interval past
129
+ the 10,000 it may return, instead of reading the whole window first. `store.listBusy` takes a
130
+ `limit`.
131
+ - The recurrence budget counts work, not time, and now charges what each step costs, time zone
132
+ reads included. Fifty writes of `FREQ=DAILY;INTERVAL=9999;BYMONTH=2;BYMONTHDAY=29` held the
133
+ server for 23 seconds, and forty daily London series held a feed for 35 seconds; a refused write
134
+ now ends within about a tenth of a second, and a feed within about half a second.
135
+ - A feed event may list 8 RRULEs, and each one past the first costs a step at every occurrence it
136
+ passes. One event with 110,000 RRULEs (2 MB) held `parseICalendarFeed` for about 9 seconds.
137
+ - One recurring series can no longer stop its tenant's maintenance. A rule naming two wall times in
138
+ a spring-forward gap (`FREQ=DAILY;BYHOUR=2,3;BYMINUTE=0,30` in New York) gave one instant twice,
139
+ its roll failed every pass, and no other series of the tenant extended or had its expired holds
140
+ purged. An instant is now one occurrence wherever the rule names it again, and a series whose
141
+ roll fails for any other reason is refused and put back (Upgrade notes).
142
+ - An event's title, description, location and participant names may hold a tab or a line break,
143
+ and no other control character; its UID none. ical.js left a bare CR in what `emitICalendar` and
144
+ `emitITipCalendar` wrote, so a reader that ends lines at one read the rest as properties of its
145
+ own (`METHOD:CANCEL`). A CRLF line break is kept as LF.
146
+ - The Node listener refuses a request target with a dot segment between backslashes
147
+ (`/v1/resources\..\availability`), as it refuses one between slashes: URL parsing reads a
148
+ backslash as a slash. An encoded backslash (`%5C`) is still an ordinary character.
149
+ - A write confirmation no longer states a series' dates in UTC when it could not read the event (a
150
+ write-only key, a refused or slow read) and the update names no time zone. The store books the
151
+ series in the event's own zone, so the dates approved could differ from those booked. The
152
+ question now shows the rule and says its dates follow the event's existing zone.
153
+ - An event whose organizer or attendee address the store keeps but `z.email()` refuses, such as
154
+ `ops@localhost` from an imported feed, no longer makes every listing of its window answer 500 and
155
+ stop paginating (Upgrade notes).
156
+ - Dashboard sign-ins are counted per client network, in the Worker and in `slotlock serve`: an
157
+ IPv4 address, or an IPv6 client's /48, so a site rotating through its addresses cannot use up the
158
+ ceiling every other client shares. Every other request counts an IPv6 client as its /64. The
159
+ dashboard runs at most two sign-ins from one client at once.
160
+ - The example dashboard Worker configuration removes query strings, which hold OAuth codes, from
161
+ the URLs Workers Logs keep.
162
+ - SECURITY.md and the README say write confirmation is attested by the client: it keeps an agent
163
+ from writing unasked and is not an authorization boundary. SECURITY.md also says how hosted
164
+ Slotlock may be tested: your own account only, no other account's data, no load or denial of
165
+ service, reports through private vulnerability reporting.
166
+ - Development lockfiles only (nothing reaches the package or the image): the site's and the
167
+ dashboard Worker's Wrangler move undici from 7.29.1 to 7.30.1 (GHSA-xjj6-f33q-929c, high), and
168
+ the development lockfile moves body-parser from 2.3.0 to 2.4.0 (GHSA-27r3-jxmh-p828).
169
+
170
+ ### Added
171
+
172
+ - The application API serves the store calls it lacked, each in `openapi.json` and the five
173
+ clients: a resource's `visibility` (`private` ones are never shown to agents) and `archived_at`;
174
+ `POST /v1/resources/{id}/archive` and `…/restore`, and `DELETE /v1/resources/{id}` (`409
175
+ resource_in_use` while anything on it is live); `GET /v1/holds/{id}`,
176
+ `GET /v1/resources/{id}/holds`, `GET /v1/reservations/{ref}`,
177
+ `GET /v1/resources/{id}/reservations` and `GET /v1/resources/{id}/busy`, paged and bounded to
178
+ 367 days; `GET /v1/tenant/events` and `GET`/`DELETE /v1/tenant/events/{id}`, which reach and
179
+ cancel any key's event, a revoked one's included; and `GET /v1/tenant`, the key's tenant and
180
+ whether it is suspended.
181
+ - `POST /v1/tenant/erase` erases the key's tenant, with its reference as `confirm_tenant_ref`. It is
182
+ off unless the application backend is created with `tenantErasure: true` (`slotlock serve` leaves
183
+ it off) and refused while the tenant is suspended.
184
+ - `POST /v1/holds` and `DELETE /v1/tenant/events/{id}` take a retry identity as the
185
+ `Idempotency-Key` header as well as `idempotency_key`; a hold retried with it answers the first
186
+ hold, and the key reused with another request is `409 idempotency_conflict`. Another application
187
+ route refuses the header (`400 idempotency_key_not_supported`) rather than ignore it.
188
+ - `POST /v1/holds` takes `buffer_after_minutes` (0 to 30 days), as a booking does: the time after
189
+ the hold stays busy, and the booking it is confirmed as keeps it.
190
+ - In the store: private resources, which agents neither list, describe nor book; `archiveResource`,
191
+ `restoreResource` and `deleteResource`; holds with a turnaround buffer the booking keeps and a
192
+ retry identity; `getHold`, `listHolds`, `getReservation` and `listReservations`; and reading,
193
+ listing and cancelling any key's event as the tenant.
194
+ - Tenant limits in the store (`tenantLimits`: resources, holds, reservations, events, coverage),
195
+ refused with `*_limit_reached`. They count what is live or still to come plus what the tenant
196
+ added in the last 30 days, so ended history leaves them. Maintenance now deletes expired holds.
197
+ - Tenant suspension and erasure: the deployment role may `suspendTenant` (with a reason) and
198
+ `unsuspendTenant`; a suspended tenant may read, its writes are refused `tenant_suspended`, and it
199
+ gets no new API key. `eraseTenant` deletes every row a tenant owns in one call and says how many
200
+ from each table. No serving role may suspend.
201
+ - `slotlock resource archive|restore|delete <ref>` and `slotlock tenant status|suspend|unsuspend|erase`.
202
+ `suspend` and `unsuspend` connect as `SLOTLOCK_MIGRATE_DATABASE_URL`'s role, the only one that
203
+ may; `erase` needs `--confirm` repeating `SLOTLOCK_TENANT`.
204
+ - `slotlock_update_event` takes `expected_resource_id`: an event since moved to another resource is
205
+ not found, checked under the event's lock.
206
+ - `slotlock serve` limits requests: per client address before any key is looked up, then per API
207
+ key, per tenant and for the whole server, with reads and writes counted apart (by default 20
208
+ reads and 5 writes a second per key and per tenant). The application API counts in its tenant's
209
+ own budget (`app_read`, `app_write`). A refusal answers 429 `rate_limited` with `Retry-After` and
210
+ the `RateLimit` and `RateLimit-Policy` fields. `SLOTLOCK_LIMITS` changes the limits. Behind a
211
+ proxy without `SLOTLOCK_TRUSTED_PROXY_HEADER`, the first request carrying a client-address field
212
+ logs a `client_address_field_ignored` warning. The health check is not counted, and reuses one
213
+ database check a second.
214
+ - `slotlock serve` runs a recurring create and every event update one at a time per tenant and per
215
+ server (`tenant_series_writes`, `series_writes`), taking the write's token before it waits; each
216
+ server process counts its own slots. A freed slot rests for as long as the server was busy while
217
+ the write before held it, up to a second, so writes that each hold it under a second keep the
218
+ server busy at most about half the time. It then goes to the waiting tenant with the fewest
219
+ writes in flight and, among those, the one whose writes have kept the server busy least lately.
220
+ Tenants with more writes waiting never go ahead of one sending a write at a time, but many
221
+ tenants with one write each and less use, new ones included, can each go ahead of it once. Four
222
+ more of a tenant's writes may wait and the rest get 429. When the server's 64 places are taken,
223
+ a write from a tenant with fewer writes in flight than the one with the most takes the place of
224
+ that tenant's last write, which gets 503 `server_busy`. One that has waited two seconds gets 429
225
+ while its tenant's own write is still running, otherwise 503 `server_busy`. An application API
226
+ maintenance run waits the same way for slots of its own: one per tenant, `maintenance_runs` (1)
227
+ per server.
228
+ - `createSlotlockAgentServer` takes `subscriptions.maxPerTenant`, which `slotlock serve` sets from
229
+ `tenant_subscriptions`.
230
+ - `slotlock serve` logs a `refusals` line each minute there are any, and at shutdown: auth
231
+ refusals, rate limits, lookup overflows, busy series writes, busy maintenance runs and bodies too
232
+ large, with clients named only by per-process pseudonyms.
233
+ - `createSlotlockNodeServer` hands the fetch target the connection's remote address as a second
234
+ argument.
235
+ - `parseICalendarFeed` reads a feed series by series and names the series it refuses, with why,
236
+ instead of refusing the whole calendar; the worker pool offers it too.
237
+ `recurrenceRuleProblem(rule)` says why a rule shape is refused, and
238
+ `storedCalendarEventProblem(event)` why a stored event would be.
239
+ - An exported calendar carries a VTIMEZONE for each IANA zone it names, so a reader that does not
240
+ know the name reads the times as Slotlock does.
241
+ - The npm package ships `openapi.json`, the REST API's OpenAPI 3.1 document, exported as
242
+ `slotlock/openapi.json`. The capability manifest names the 0.2 surfaces: `rest-api`,
243
+ `application-api`, `api-keys`, `dashboard` and `maintenance`.
244
+ - The five language clients are published with each release: `slotlock` on PyPI and crates.io,
245
+ `Slotlock.Client` on NuGet, `io.pylota:slotlock` on Maven Central and the Go module's
246
+ `clients/go/v<version>` tag, each after the release suite passes and a maintainer approves. They
247
+ now carry the package's version (they said 0.1.0 beside a 0.2.0 server).
248
+ `docs/release/clients.md` has each registry's one-time setup.
249
+ - `scripts/check-stage.sh <stage-id>`, which a maintainer runs before `npm stage approve`. It
250
+ refuses a stage whose tarball is not the one the release tested, whose dist-tag would move
251
+ `latest` backwards, or whose runtime dependencies, installed as a consumer would that day, have a
252
+ high or critical advisory. 0.2.0 went public with zod 4.6.5 after `main` had moved past it.
253
+ - A known-issues notice for 0.2.0: in the README, on the site (`/docs/known-issues/`) and as text
254
+ for the v0.2.0 release notes, a pinned issue and two security advisories
255
+ (`docs/release/0.2.0-known-issues.md`). It names the six ways 0.2.0 can accept a double booking
256
+ around daylight-saving changes and series ends, and the zod pin with its workaround.
257
+ `docs/release/deprecations.md` gives the `npm deprecate` and `mcp-publisher status` commands for
258
+ 0.0.0-stage, 0.1.0, 0.1.1 and 0.2.0.
259
+ - For hosted Slotlock: drafts of a privacy notice (`PRIVACY.md`), Terms of Service (`TERMS.md`,
260
+ with data processing terms) and an Acceptable Use Policy (`ACCEPTABLE-USE.md`), each marked for
261
+ owner and legal review; a service page (`docs/hosted/README.md`) with addresses, price, limits
262
+ and contacts; `VERSIONING.md`, what `/v1`, the tools and the version numbers promise; a public
263
+ status from an off-server probe (`docs/hosted/status.md`, off until the owner sets
264
+ `SLOTLOCK_STATUS_PROBE`); and `.github/SUPPORT.md`, the route for each kind of request.
265
+
266
+ ### Changed
267
+
268
+ - A recurring event occupies both readings of each occurrence across a clock change: RFC 5545's
269
+ exact duration from its start, and its written end on the wall clock. A start in a skipped hour
270
+ moves forward by the gap, and one in a repeated hour takes the first instant. One-off events,
271
+ overrides and PERIOD RDATEs keep their exact times. Feeds, the trusted importer and the store all
272
+ read events this way.
273
+ - A stored series is expanded from the instants it holds, and its exceptions name occurrences by
274
+ UTC instant (exported as UTC `RECURRENCE-ID`s).
275
+ - A series' occurrences never overlap each other: one that reaches past the next one's start ends
276
+ there, and the next ends at the later of the two ends.
277
+ - Recurring series are stored for up to 369 days (`SLOTLOCK_MATERIALIZATION_WINDOW_DAYS`), so a
278
+ full 367-day search can be complete; search, free/busy and coverage windows keep their 367-day
279
+ cap.
280
+ - A booking, hold or event first extends each series on its resource that is not stored as far as
281
+ it reaches, so it meets their occurrences. A write past the rolling horizon on a resource whose
282
+ series goes on past it is refused with `recurrence_not_materialized`; a series known to have
283
+ ended does not count.
284
+ - A confirmed hold becomes a booking of its tenant whose reference is the hold's id, at revision 1,
285
+ so it can be read, moved and cancelled like any booking (`slotlock_confirm_hold` says so).
286
+ `createReservation` on a tenant's resource does the same. The migration, and maintenance for
287
+ rows a 0.2 server writes during a rolling deploy, give older such bookings their tenant.
288
+ - `server.json` (the MCP Registry entry) no longer marks `SLOTLOCK_AUTH_TOKEN` and
289
+ `SLOTLOCK_CONFIRMATION_SECRET` as required, and its Authorization header takes the token or an
290
+ API key.
291
+ - `slotlock --help` describes the server token as read and write, never admin.
292
+ - Releases: npm's `latest` dist-tag, and the image's `latest` tag, never move backwards. A release
293
+ on an older line takes `latest-<major>.<minor>` and a prerelease `next`, and a release stops when
294
+ the registry cannot say which version is `latest`. The image and the MCP Registry entry are
295
+ published only after the release's own suite passes, each behind an approval; the image's
296
+ attestation is still signed by `image.yml`. The packed NOTICE is written at build time from the
297
+ lockfile, so a dependency bump no longer breaks the release checks. **Release a version** takes
298
+ `dry_run`.
299
+ - Repository: CI runs the database suites on PostgreSQL 16, 17 and 18 and the Compose quickstart
300
+ end to end, and one check, "CI passed", gathers every job, the language clients included.
301
+ `scripts/configure-github.sh` requires it and DCO's "Signed-off-by" on `main` with no admin
302
+ bypass, puts the person who runs it as reviewer on the publishing environments, and sets up
303
+ CodeQL and Dependabot for every manifest.
304
+
305
+ ### Fixed
306
+
307
+ - 0.2.0's six double-booking cases (Known issues in the README): a first occurrence spanning a
308
+ clock change, the repeated hour of an autumn change in stored series, exceptions in a skipped
309
+ hour, occurrences starting in a skipped hour, a timed series with a date-only `UNTIL` (now its
310
+ whole day on the series' clock), and an iCalendar `DURATION` counting hours across a spring
311
+ change.
312
+ - A weekly-hours window whose edge falls in a skipped hour stands where the gap ends, so it holds
313
+ only the wall times it names that exist. It used to move, or give nothing.
314
+ - A yearly rule on a day some years lack (29 February) occurs in the years that have it instead of
315
+ being refused.
316
+ - A feed's daily or weekly series from years ago is searched from near the window, so a daily
317
+ standup since 2014 no longer refuses its feed. A CalDAV server's expanded series whose first
318
+ instance has no `RECURRENCE-ID` keeps its later instances busy.
319
+ - A series whose DTEND falls in a skipped hour no longer holds an extra hour every later day.
320
+ - Series the roll cannot extend (in conflict or over the budget) no longer keep a healthy series
321
+ from rolling, however many there are. The roll puts each behind for an hour and takes those it
322
+ has never put back first; its cursor carries that place (`rollRetryAt`), and a pass says when it
323
+ stopped before them (`rollPending`), so `slotlock serve` and `slotlock maintain` go round again
324
+ at once. The roll names the series it could not extend because of a collision
325
+ (`conflictEventIds`).
326
+ - A refused update of a recurring event names the booking in its way, not its own first
327
+ occurrence.
328
+ - The quickstart: `.env.example` turns the REST API on, as `slotlock serve` and Compose do, and
329
+ the docs say PostgreSQL 16 or newer (Compose ships 18).
330
+ - Documentation: SPEC.md and the README describe 0.2 and later, and SPEC.md gains a daylight-saving
331
+ section; the README says which spelling of an availability rule each surface takes
332
+ (`startMinutes` for `slotlock hours set` and `SLOTLOCK_AVAILABILITY`, `start_minutes` over
333
+ REST); the site names the version to install and has pages for the clients, operations and the
334
+ hosted service.
335
+
336
+ ## 0.2.0 - 2026-10-23
337
+
338
+ ### Added
339
+
340
+ - The application API: what an application does through the store, over REST from any language.
341
+ Create resources and set their weekly hours; hold, confirm and release time; book, move and cancel
342
+ reservations by reference and revision; record external calendar coverage; run maintenance. The
343
+ routes sit under `/v1` beside the tools, in the same OpenAPI document, and need an API key with the
344
+ new `admin` scope. They are never MCP tools or A2A skills. `createSlotlockStoreApplicationBackend`
345
+ serves them from the store; `createSlotlockAgentServer` takes them as `application`.
346
+ - The `admin` API key scope: `slotlock key create <name> --scope read,write,admin`, or "Full" in the
347
+ dashboard. New keys still default to read and write, and `SLOTLOCK_AUTH_TOKEN` is never admin: it
348
+ is the token agents are given, and the application API asks no person before it writes.
349
+ - Built-in maintenance. `slotlock serve` maintains every tenant it serves, hourly, one at a time,
350
+ under a per-tenant advisory lock, and logs the counts; `slotlock maintain` runs it once.
351
+ `maintainSlotlockTenant`, `startSlotlockMaintenance` and `withSlotlockMaintenanceLock` do the
352
+ same in a process of your own.
353
+ - `confirmHold` and `releaseHold` take `{ tenantRef }`, so another tenant's hold is not found
354
+ whatever the connection's row-level security.
355
+ - Clients for Python, Go, Rust, Java and .NET (`clients/`): one method per REST operation, the
356
+ application API included, generated from `openapi.json` with hand-written transports. Each runs
357
+ the same scenario against the server image in CI. They are not on their package registries yet.
358
+ - `openapi.json`, the REST API's OpenAPI 3.1 document, in the repository and at
359
+ https://slotlock.pylota.io/openapi.json; `slotlockOpenApi()` builds it.
360
+ - Property-based tests (fast-check) of the engine, time zones, iCalendar parsing and API keys.
361
+ - A `call` event for each MCP `tools/call` and A2A `SendMessage` that names an operation: its
362
+ outcome, duration and W3C trace id. `rest` events carry the duration and trace id too, and
363
+ `slotlock serve` logs both.
364
+ - Releases are opened and merged by a GitHub App once it is set up
365
+ (`node scripts/create-release-app.mjs`), so its checks run without a maintainer approving them.
366
+ Only the commit whose checks passed is merged.
367
+
368
+ ### Changed
369
+
370
+ - `slotlock serve` serves REST by default; `SLOTLOCK_REST_API=off` turns it off.
371
+ - `expandRules` and `findNextAvailableFor` refuse a search window longer than 367 days with
372
+ `invalid_window`, as free/busy already did, and skip a rule that lasts longer than a week, as the
373
+ hours validator refuses one. Fifty daily rules over a century were two million windows of
374
+ synchronous work.
375
+ - Upgrading rebuilds the unique index on booking references
376
+ (`slotlock_reservations_booking_external_ref_key`) to leave calendar event rows out, and drops
377
+ the one it replaces after the new one is built.
378
+ - A cursor 0.1 kept beside a stored series is not resumed if the series is zoned and ends (UNTIL):
379
+ 0.1 read its occurrences as UTC beside the UNTIL, so the cursor may have stopped one occurrence
380
+ early (a booking could then take that time) or gone one past. The next roll searches such a series
381
+ from its start, within the same budget, and keeps a new cursor.
382
+ - Upgrading widens the API key scope constraint in place, and replaces the reservations overlap
383
+ constraint with one that counts every row's turnaround buffer (below). Expired holds in a buffer's
384
+ way are removed; while a live hold or untenanted reservation is in one, migrate stops with their
385
+ ids and changes nothing.
386
+
387
+ ### Fixed
388
+
389
+ - A calendar event's occupation could be freed or moved through the reservation commands
390
+ (`cancelExternalReservation`, `rescheduleExternalReservation`, and the application API's
391
+ reservation routes) by naming the reference the store derives for it: the event stayed active but
392
+ stopped occupying its time. Those commands now leave event rows out. An event freed this way
393
+ before upgrading occupies its time again once it is written again.
394
+ - Maintenance replaced a series' stored window with the rolling horizon even when the stored window
395
+ ended later, deleting the occurrences past it.
396
+ - Free/busy called a window complete when a series started inside it but was stored only from
397
+ later: those occurrences were reported as free time.
398
+ - Upgrading as a non-superuser owner under forced row-level security failed with a raw error (or,
399
+ for some repairs, skipped them): the upgrade's data steps saw no rows. The upgrade now lifts FORCE
400
+ for those steps and records what it lifted, so an upgrade stopped part way is put right by the
401
+ next one, and `slotlock serve` refuses to start while a tenant table is left without FORCE.
402
+ - `expandRules` without a time zone offered windows after the rule's UNTIL.
403
+ - A delayed copy of a create, after the reservation had moved on, answered `idempotency_conflict`
404
+ instead of `stale_revision`.
405
+ - A time zone a calendar names without a VTIMEZONE was read as floating, and compared with UTC
406
+ values as if it were UTC. East of UTC a series lost the occurrence its UNTIL names (in a stored
407
+ series, a booking could then take that time); west of it, a series ran hours past its UNTIL; an
408
+ EXDATE or a moved occurrence's RECURRENCE-ID written in UTC was not found. All-day series had the
409
+ same fault with a UTC UNTIL.
410
+ - A feed with a PERIOD RDATE, or an event with no DTEND, was refused whole; a feed's event that
411
+ ended before it started was refused with `invalid_external_interval` instead of
412
+ `invalid_icalendar`; a document whose root is not a VCALENDAR read as an empty calendar.
413
+ - The 4,000 occurrences a series may pass from its start were shared by every series in a feed, so
414
+ three daily series begun in 2023 refused it for good.
415
+ - An EXDATE left its occurrence in when EXDATEs before it named no occurrence (ical.js passes them
416
+ one per occurrence), or when an RRULE and an RDATE both named it; such an instant was also
417
+ reported twice. A series of RDATEs with no RRULE left out its DTSTART, its first occurrence.
418
+ - An occurrence starting in an hour the clocks skip ended as it started and was left out; it now
419
+ lasts its series' exact length, from its DTEND or its DURATION. Others still end on the wall clock.
420
+ - ical.js matched exceptions and compared UNTIL and EXDATE values with a feed's own VTIMEZONE for an
421
+ IANA name, while Slotlock read the same times through Intl: under a stale definition, a moved
422
+ occurrence was found in the feed and lost from the series stored from it. Such a name is read
423
+ through Intl throughout.
424
+ - A series with two RRULEs lost occurrences once either rule ended: ical.js dropped the rule after
425
+ an ended first one, or the one before any other.
426
+ - A DATE EXDATE of a timed series excluded every occurrence that day from a feed and none from the
427
+ series stored from it; both now exclude the occurrence at the series' start time that day.
428
+ - A floating UNTIL was read as UTC beside a zoned DTSTART. A RECURRENCE-ID written in another zone
429
+ than its series' DTSTART did not find its occurrence, and a `RANGE=THISANDFUTURE` one written in
430
+ UTC moved later occurrences by the wrong amount.
431
+ - `parseTrustedICalendarEvents` did not read times as `parseICalendarChanges` does: a stored
432
+ all-day series kept an occurrence the feed's UTC EXDATE excludes. An event it cannot hold (no
433
+ length, say) is now `invalid_icalendar`, not `invalid_external_interval`.
434
+ - A feed naming thousands of time zones it does not define (50,000 spellings of one zone took
435
+ seconds) is refused past 100, as one defining more than 100 is; a feed listing too many EXDATE
436
+ values is refused before they are read, not after.
437
+ - A request body sent without `Content-Length` (chunked) was read whole before the size limit
438
+ applied, so any key holder could make the server hold gigabytes. It is refused as soon as it
439
+ passes `maxRequestBytes`. The Node adapter no longer aborts a request whose body the server stops
440
+ reading; it discards the rest once it has answered.
441
+ - `close()` while the Node adapter was still starting left `listen()` pending forever.
442
+ - On the Workers dashboard, `/dashboard/sign-in/` and `/dashboard/callback/` (with a trailing slash)
443
+ were outside the global sign-in rate limit.
444
+ - The confirmation question for a create did not say that a transparent booking leaves the time
445
+ free, or name the other fields it sets.
446
+ - `arguments` that were not an object (a string, an array) ran the operation with none; they are
447
+ now `-32602` on MCP and an invalid message on A2A. Left out or null is still none.
448
+ - A rate-limited A2A request was answered with an MCP error over HTTP 429; it is now A2A's 200
449
+ envelope with a `RATE_LIMITED` ErrorInfo. `Content-Type` is matched without regard to case.
450
+ - Calls refused before they ran (a client that cannot confirm, a malformed A2A message, a REST body
451
+ that is not JSON, too large or not an object) were not reported to `onEvent`. On MCP 2026-07-28
452
+ a confirmation was reported after the call it decided; it now comes first, as elsewhere.
453
+ - An async `onEvent`, `onError` or maintenance `onPass` that rejected was an unhandled rejection,
454
+ which ends a Node process by default; an `onPass` that threw made a pass that ran count as failed.
455
+ - A hold could take the time a reservation's `bufferAfterMs` keeps busy, and a buffered reservation
456
+ could be booked over a hold: the buffer bound only tenant-owned reservations to each other.
457
+ - `confirmHold` with the id of a reservation that has an external reference answered as if it had
458
+ confirmed a hold, describing it without its reference, revision or buffer. It is now
459
+ `hold_not_found`.
460
+
6
461
  ## 0.1.1 - 2026-10-22
7
462
 
8
463
  ### Changed
package/CONTRIBUTING.md CHANGED
@@ -6,8 +6,8 @@ listed in [MAINTAINERS.md](./MAINTAINERS.md).
6
6
 
7
7
  ## Development
8
8
 
9
- You need Node.js 22.12 or newer and, for the database suites, PostgreSQL 16. From the repository
10
- root:
9
+ You need Node.js 22.12 or newer and, for the database suites, PostgreSQL 16 or newer (CI runs them
10
+ on 16, 17 and 18). From the repository root:
11
11
 
12
12
  ```sh
13
13
  npm ci
@@ -15,8 +15,9 @@ npm run typecheck # the package, then examples/ as a consumer compiles them
15
15
  npm test # tests, build, then the packed-artifact verification
16
16
  ```
17
17
 
18
- Database suites (`*.integration.test.ts`) run when `DATABASE_URL` points at a PostgreSQL 16
19
- database whose connecting role can create roles and extensions, and are skipped otherwise:
18
+ Database suites (`*.integration.test.ts`) run when `DATABASE_URL` points at a PostgreSQL 16 or newer
19
+ database whose connecting role can create roles, databases and extensions, and are skipped
20
+ otherwise (the upgrade suite builds a database of its own and drops it):
20
21
 
21
22
  ```sh
22
23
  DATABASE_URL=postgresql://slotlock_dev:dev-only-password@localhost:5432/slotlock_test npm test
@@ -29,6 +30,11 @@ role named `slotlock` is the package's schema, which `"$user"` puts first on the
29
30
  command-line tool redacts the database password from everything it prints, so a short or
30
31
  product-like password rewrites the output the tests read.
31
32
 
33
+ Property suites (`*.property.test.ts`) check the engine, time zones, iCalendar parsing and API
34
+ keys on input [fast-check](https://fast-check.dev/) generates, with a new seed each run. A failure
35
+ prints the seed and the shrunk counterexample; add `{ seed, path }` from it to that `fc.assert` to
36
+ replay it, then keep the counterexample as an ordinary test once it is fixed.
37
+
32
38
  ## Sign off your commits
33
39
 
34
40
  Slotlock uses the [Developer Certificate of Origin](https://developercertificate.org/) instead of a
@@ -59,6 +65,11 @@ Edit the example, not the README copy: `readme-examples.test.ts` fails when they
59
65
  - Run typecheck, tests, build, and package verification.
60
66
  - Call out database, tenancy, privacy, sync, or supply-chain risk explicitly.
61
67
  - Do not add provider credentials, product-specific authorization, or network calls to the core.
68
+ - `NOTICE` is written by `node scripts/notice.mjs --write`. A dependency bump needs no edit to it,
69
+ unless it adds or removes a runtime dependency in `package.json` or a dependency changes its
70
+ licence: then CI fails with "NOTICE is not what scripts/notice.mjs writes". Read the new
71
+ licence, then run that command. A licence `scripts/notice.mjs` has not reviewed stops the build
72
+ until it is added to `LICENSE_NAMES` there.
62
73
 
63
74
  Changes to exclusion constraints, RLS, identity/idempotency, retention, iCalendar trust boundaries,
64
75
  protocol error contracts, or release automation require a security review; while the project has
@@ -71,21 +82,46 @@ that section becomes the release notes.
71
82
 
72
83
  To release, run **Actions > Release a version** with the version (for example `0.1.1`). It sets the
73
84
  version in `package.json`, `package-lock.json`, `server.json` and `slotlock.manifest.json` and dates
74
- the `## Unreleased` heading (`scripts/prepare-release.sh`) and opens the pull request. GitHub holds
75
- the checks of a pull request a workflow opens until someone with write access approves them: open it
76
- and click **Approve workflows to run**. The run then merges it once CI and DCO pass on it, and starts
77
- `tag-release.yml`, which tags the release commit. The tag starts
78
- `release.yml` (the suite, then npm and the GitHub release) and `image.yml` (the image, then the MCP
79
- Registry). npm holds each staged version until a maintainer approves it with two-factor
80
- authentication (GOVERNANCE.md); the Release run's summary gives the command. A version merged to
81
- `main` by hand, with its heading dated, is released the same way. After a failed run, run it again
82
- with the same version: it keeps the release branch the failed run left when that already holds this
83
- release, so checks approved there still count, replaces it otherwise, and reuses its pull request
84
- (`scripts/open-release-pr.sh`).
85
-
86
- GitHub lets a workflow open pull requests only once an admin allows it (Settings > Actions >
87
- General > Workflow permissions); `scripts/configure-github.sh` sets it, and the run says so when it
88
- is off.
85
+ the `## Unreleased` heading (`scripts/prepare-release.sh`), opens the pull request, merges it once CI
86
+ and DCO pass on it, and `tag-release.yml` tags the release commit. The tag starts `release.yml`: the
87
+ suite, then npm and the GitHub release, and, only once the suite has passed, `image.yml` (the image,
88
+ then the MCP Registry). The image and the registry entry each wait for a maintainer to approve their
89
+ environment (`ghcr`, `mcp-registry`), and npm holds each staged version until a maintainer approves
90
+ it with two-factor authentication (GOVERNANCE.md); the Release run's summary gives the command. A version merged to `main` by hand, with its heading
91
+ dated, is released the same way. After a failed run, run it again with the same version: it keeps
92
+ the release branch the failed run left when that already holds this release, so checks already run
93
+ there still count, replaces it otherwise, and reuses its pull request (`scripts/open-release-pr.sh`).
94
+
95
+ The pull request is opened and merged by the release app, a GitHub App that can only write this
96
+ repository's contents and pull requests. An owner of the organization creates it once with
97
+ `node scripts/create-release-app.mjs` (click **Create GitHub App**, then install it on `slotlock`
98
+ only); the script checks the installation and stores the app's key in the `release-app` environment,
99
+ which only `main` can use. Without the app, the workflow opens the pull request as
100
+ github-actions[bot], whose checks GitHub holds until someone with write access approves them on the
101
+ pull request (**Approve workflows to run**). GitHub lets a workflow open pull requests at all only
102
+ once an admin allows it (Settings > Actions > General > Workflow permissions);
103
+ `scripts/configure-github.sh --allow-actions-pull-requests` sets it, and the run says so when it is
104
+ off. By default the script leaves it off, since the same switch lets GITHUB_TOKEN approve pull
105
+ requests.
106
+
107
+ Before the first release through the app, or after replacing it, run **Release a version** with
108
+ **dry_run**: it mints the app's token and prepares the release as a real run would, checks the token
109
+ can read the repository and push the release branch (`scripts/release-dry-run.sh`), and stops
110
+ without pushing, opening, merging or tagging anything. Its summary says which identity a real run
111
+ would use.
112
+
113
+ Nobody, the admin included, can merge into `main` past a failing or missing required check ("CI
114
+ passed" and "Signed-off-by"): a change whose checks cannot pass is fixed in another pull request
115
+ first. The npm, `ghcr` and `mcp-registry` environments wait for a maintainer's approval in every
116
+ Release run.
117
+
118
+ Approving a stage publishes the tarball the tag's run built, however long ago that was. Before
119
+ `npm stage approve`, run `scripts/check-stage.sh <stage-id>` from a checkout, logged in to npm and
120
+ gh. It refuses a stage whose tarball is not the one the release tested (the GitHub release's
121
+ `SHA256SUMS`), whose dist-tag would move `latest` backwards, or whose runtime dependencies, as a
122
+ consumer installs them that day, have a high or critical advisory (`npm audit --omit=dev`). Reject
123
+ such a stage and release a version that fixes it; accept a named advisory with
124
+ `--allow <GHSA id>` only when the release notes say why. It approves nothing itself.
89
125
 
90
126
  When a run needs a step npm or GitHub keeps for a person (the first npm version, the npm trusted
91
127
  publisher, the image's visibility, and the deploy key `tag-release.yml` pushes tags with), the
package/GOVERNANCE.md CHANGED
@@ -42,8 +42,10 @@ Releases follow semantic versioning and are built only by the repository release
42
42
  protected `v<version>` tag on `main`. The tag must match `package.json` and a `CHANGELOG.md`
43
43
  heading. The workflow stages the tested artifact to npm with trusted publishing (short-lived OIDC
44
44
  credentials and provenance); a maintainer then approves the staged version with two-factor
45
- authentication. Long-lived npm tokens are not accepted. Release artifacts must pass the
46
- package-content and consumer checks.
45
+ authentication. The same run publishes the server image and the MCP Registry entry only after its
46
+ suite has passed, and each waits for a maintainer to approve it. Long-lived npm tokens are not
47
+ accepted. Release artifacts must pass the package-content and consumer checks. Required checks
48
+ apply to maintainers too: nobody merges into `main` past a failing or missing one.
47
49
 
48
50
  ## Project changes
49
51
 
package/MAINTAINERS.md CHANGED
@@ -13,8 +13,9 @@ normally need two maintainer approvals are made until a second maintainer joins.
13
13
  [SECURITY.md](./SECURITY.md). Never a public issue.
14
14
  - **Code of conduct concerns:** [clever.eziogor@pylota.io](mailto:clever.eziogor@pylota.io), as described in
15
15
  [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md#reporting).
16
- - **Everything else:** an issue or discussion on
17
- [github.com/PILOTAAI/slotlock](https://github.com/PILOTAAI/slotlock).
16
+ - **Hosted Slotlock** (support, abuse, privacy): the addresses in
17
+ [.github/SUPPORT.md](.github/SUPPORT.md).
18
+ - **Everything else:** an issue on [github.com/PILOTAAI/slotlock](https://github.com/PILOTAAI/slotlock).
18
19
 
19
20
  ## Becoming a maintainer
20
21
 
package/NOTICE CHANGED
@@ -5,16 +5,16 @@ This product includes software developed by TREFT LTD and the Slotlock contribut
5
5
  under the Apache License 2.0.
6
6
 
7
7
  Runtime dependencies are not bundled into the Slotlock artifact; npm installs them alongside it and
8
- each retains its own license:
8
+ each retains its own license. This release was built with these versions:
9
9
 
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
- - tslib (via rrule), BSD Zero Clause License (0BSD), https://github.com/microsoft/tslib
13
- - zod 4.6.5, MIT License, https://github.com/colinhacks/zod
12
+ - tslib (via rrule), BSD Zero Clause License (0BSD), https://github.com/Microsoft/tslib
13
+ - zod 4.6.7, MIT License, https://github.com/colinhacks/zod
14
14
 
15
15
  Peer dependency, installed and versioned by the application that embeds Slotlock:
16
16
 
17
- - Postgres.js (postgres) ^3.4.5, The Unlicense, https://github.com/porsager/postgres
17
+ - postgres ^3.4.5, The Unlicense, https://github.com/porsager/postgres
18
18
 
19
19
  If Slotlock distributes a modified MPL-covered ical.js file in the future, that file and its source
20
20
  must remain available under MPL-2.0. Slotlock currently imports the unmodified external dependency.