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