slotlock 0.6.2 → 0.7.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 +492 -0
- package/CONTRIBUTING.md +12 -5
- package/README.md +50 -1544
- package/SECURITY.md +5 -2
- package/SPEC.md +1 -1
- package/agent-server.d.ts +160 -8
- package/agent-server.js +560 -49
- package/agent-store-backend.d.ts +39 -7
- package/agent-store-backend.js +1439 -104
- package/api-keys.d.ts +69 -3
- package/api-keys.js +218 -29
- package/application-api.d.ts +59 -6
- package/application-api.js +842 -39
- package/approvals.d.ts +88 -0
- package/approvals.js +293 -0
- package/assistant.d.ts +58 -27
- package/assistant.js +151 -53
- package/billing.d.ts +114 -0
- package/billing.js +258 -0
- package/booking-rules.d.ts +30 -41
- package/booking-rules.js +17 -25
- package/change-watch.d.ts +1 -0
- package/change-watch.js +1 -1
- package/cli.js +3 -4
- package/config.d.ts +42 -6
- package/config.js +114 -7
- package/dashboard-icon.d.ts +1 -0
- package/dashboard-icon.js +5 -0
- package/dashboard.d.ts +203 -7
- package/dashboard.js +1558 -64
- package/ddl.d.ts +67 -35
- package/ddl.js +2732 -162
- package/engine.d.ts +6 -9
- package/engine.js +5 -9
- package/event-feed.d.ts +53 -35
- package/event-feed.js +201 -64
- package/event-schema.js +6 -9
- package/hooks.d.ts +2 -4
- package/hooks.js +2 -4
- package/ical-worker-thread.js +3 -4
- package/ical-worker.d.ts +8 -10
- package/ical-worker.js +7 -9
- package/index.d.ts +15 -4
- package/index.js +8 -2
- package/interop.js +1 -4
- package/maintenance.d.ts +24 -10
- package/maintenance.js +59 -28
- package/mcp-app.d.ts +7 -9
- package/mcp-app.js +7 -9
- package/mcp-modern.d.ts +14 -16
- package/mcp-modern.js +23 -27
- package/metrics.d.ts +41 -0
- package/metrics.js +22 -1
- package/node-server.d.ts +9 -9
- package/node-server.js +25 -34
- package/openapi.json +14675 -5957
- package/otel.d.ts +86 -0
- package/otel.js +273 -0
- package/overage.d.ts +33 -0
- package/overage.js +115 -0
- package/package-version.js +2 -4
- package/package.json +1 -1
- package/partner-billing.d.ts +81 -0
- package/partner-billing.js +333 -0
- package/partner-names.d.ts +8 -0
- package/partner-names.js +10 -0
- package/partners.d.ts +147 -0
- package/partners.js +375 -0
- package/plans.d.ts +92 -0
- package/plans.js +115 -0
- package/quotas.d.ts +108 -0
- package/quotas.js +258 -0
- package/rate-limit.d.ts +27 -2
- package/rate-limit.js +106 -23
- package/resource-profile.d.ts +135 -0
- package/resource-profile.js +263 -0
- package/rest-api.d.ts +15 -14
- package/rest-api.js +79 -15
- package/rules.d.ts +6 -7
- package/rules.js +30 -42
- package/sandbox.d.ts +70 -0
- package/sandbox.js +295 -0
- package/self-host.d.ts +46 -4
- package/self-host.js +990 -41
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +482 -165
- package/store.js +1460 -484
- package/sync.d.ts +44 -59
- package/sync.js +325 -469
- package/teams.d.ts +25 -0
- package/teams.js +63 -0
- package/timezone.d.ts +25 -33
- package/timezone.js +50 -53
- package/types.d.ts +130 -57
- package/usage.d.ts +106 -0
- package/usage.js +238 -0
- package/webhooks.d.ts +22 -25
- package/webhooks.js +35 -40
- package/weekly-hours.d.ts +3 -0
- package/weekly-hours.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,498 @@
|
|
|
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.7.0 - 2026-11-10
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- The sandbox (RFC 0004 A6), in a test environment only: seed a scenario (car rental, clinic,
|
|
11
|
+
desks) into an empty one, race agents for one slot and report who booked, who was refused and
|
|
12
|
+
how long each answer took, fast-forward (holds, waitlist offers and approvals that would lapse
|
|
13
|
+
within some minutes lapse now, then maintenance reaps them), and reset (everything but its keys,
|
|
14
|
+
webhooks, plan and usage). `slotlock sandbox …`, four admin operations (`/v1/sandbox/*`) and the
|
|
15
|
+
dashboard's Test overview. Each refuses a live tenant, and so does the database's
|
|
16
|
+
`slotlock.reset_test_tenant`, which acts only in the caller's own test tenant.
|
|
17
|
+
- Contention at scale (RFC 0004 A7): a CI test races 100 REST clients for one slot (one booking, 99
|
|
18
|
+
`overlap` refusals with alternatives, no 5xx, p99 under a second), and the benchmark runs three
|
|
19
|
+
write workloads against Slotlock's own schema as a new column. A refusal's alternatives now come
|
|
20
|
+
from one read through the backend's optional `findNextSlots`, as do
|
|
21
|
+
`slotlock_find_available_slots`' slots, and time zone names resolve once a process.
|
|
22
|
+
- Per-key limits (RFC 0004 A5): a key can be held below the server's limits, never above them:
|
|
23
|
+
its live holds and the hours they hold, its requests a minute (a bucket of its own, `key_limit`),
|
|
24
|
+
and `requires_approval`, which makes every booking it asks for wait for a person (policy
|
|
25
|
+
`api-key`). Set with `slotlock key create`/`key limits`, the dashboard's key form and Limits
|
|
26
|
+
action, or the partner API's `limits`; kept through rotation, erased with the tenant. The
|
|
27
|
+
principal carries them as `limits`. `slotlock.api_key_limits` is owner-only behind three new
|
|
28
|
+
functions: run `slotlock migrate` (or `grantApplicationRole`) so the serving role may call them;
|
|
29
|
+
until then keys authenticate without limits.
|
|
30
|
+
- Approval policies (RFC 0004 A4): an admin names which agent bookings a person approves first, or
|
|
31
|
+
which are refused, by resource, pool, location or group, narrowed by length, time outside the
|
|
32
|
+
resource's hours and kind (single, pool, bundle, move). A covered booking is tried as a dry run,
|
|
33
|
+
then its exact time is held for the approval and the agent is answered `409 approval_pending`;
|
|
34
|
+
a grant (the dashboard's Approvals page or `slotlock_decide_approval`) books it as the agent
|
|
35
|
+
asked, in the transaction that frees the hold, and the agent's retry with the same key answers
|
|
36
|
+
the booking. Declined and expired approvals free their time. Each policy sets its own wait,
|
|
37
|
+
bounded by the booking's start less `lead_minutes` and 30 days. Six application operations
|
|
38
|
+
(`/v1/approval-policies`, `/v1/approvals`), the feed's `approval.*` changes, and
|
|
39
|
+
`approvals_expired` in maintenance's answer.
|
|
40
|
+
- Alternatives with a refusal (RFC 0004 A1): an agent write refused with `overlap`, `pool_full` or
|
|
41
|
+
a booking rule (other than `booking_rule_uncertain`) carries `alternatives`, up to three free
|
|
42
|
+
times of the same length from the time asked for, within seven days, found by
|
|
43
|
+
`slotlock_find_available_slots`' own search: on the same resource, on a bundle's resources
|
|
44
|
+
together, or on any member of the pool. `slotlock_hold_slots` gives each candidate refused for its
|
|
45
|
+
time its own. On MCP, A2A and REST alike; never a hold.
|
|
46
|
+
- Dry runs (RFC 0004 A3): every agent and application write takes `dry_run: true`. The write
|
|
47
|
+
runs with every check, the database's constraints included, then rolls back: nothing is stored,
|
|
48
|
+
recorded or sent, and the answer is what it would have been (`dry_run: true`) or the refusal,
|
|
49
|
+
alternatives included. It is never put to a person to confirm and is charged as a read (on REST,
|
|
50
|
+
with `dry_run=true` in the query string). Erasure, maintenance and the partner API answer
|
|
51
|
+
`dry_run_not_supported`.
|
|
52
|
+
- Three named booking tools (RFC 0004 A2): `slotlock_confirm_slot` books a held time exactly
|
|
53
|
+
(`POST /v1/slot-holds/{hold_id}/confirm`), `slotlock_reschedule` moves an event
|
|
54
|
+
(`POST /v1/events/{event_id}/reschedule`) and `slotlock_cancel` cancels one
|
|
55
|
+
(`POST /v1/events/{event_id}/cancel`), each with flat arguments and an idempotency key it derives
|
|
56
|
+
when none is given. The tool descriptions say what each refuses and why, in plain words, and the
|
|
57
|
+
MCP instructions walk an agent through find, hold, confirm, reschedule and cancel.
|
|
58
|
+
- Hosted plans: `slotlock.tenant_plans` (owner-only, read and set through definer functions) caps a
|
|
59
|
+
tenant's resources, active API keys and webhook endpoints. Free allows 10, 10 and 2; Developer
|
|
60
|
+
100, 100 and 10. A tenant with no plan, as on every self-hosted server, keeps the server's limits.
|
|
61
|
+
Erasure removes the plan. `readSlotlockTenantPlan` and `setSlotlockTenantPlan` in `plans.ts`.
|
|
62
|
+
Adding a resource or webhook endpoint now reads the plan through `slotlock.tenant_plan(text)`:
|
|
63
|
+
`slotlock migrate` grants it to the serving role; a library user runs `grantApplicationRole`
|
|
64
|
+
again, and a role without it is refused `application_role_not_granted`.
|
|
65
|
+
- Stripe billing (`billing.ts`) and the dashboard's Plan page, on the Worker with
|
|
66
|
+
`STRIPE_SECRET_KEY` and the Developer price ids: Upgrade opens a Stripe Checkout with Managed
|
|
67
|
+
Payments, Manage billing the customer portal, and a signed webhook at `/stripe/webhook` sets the
|
|
68
|
+
tenant's plan from the subscription Stripe holds.
|
|
69
|
+
- The dashboard serves the Slotlock mark as its favicon.
|
|
70
|
+
- A default plan in the database (`slotlock.plan_defaults`, owner-only): every tenant without a plan
|
|
71
|
+
of its own is on it from its date, through `slotlock.tenant_plan(text)`, so `slotlock serve` and
|
|
72
|
+
the dashboard Worker hold a tenant to the same limits whichever one a resource, key or webhook
|
|
73
|
+
endpoint is added through. Before the date, a new tenant (no API key, nothing in its feed, no
|
|
74
|
+
webhook) is put on it before it adds its first key, resource or endpoint
|
|
75
|
+
(`slotlock.enrol_tenant_plan(text)`); existing tenants keep their limits until the date. Set by
|
|
76
|
+
the schema owner with `slotlock plan default free --from 2026-12-09` (`none` removes it);
|
|
77
|
+
`slotlock plan status` prints it and `SLOTLOCK_TENANT`'s own plan. `readSlotlockPlanDefault`,
|
|
78
|
+
`setSlotlockPlanDefault` and `enrolSlotlockTenantPlan` in `plans.ts`. Run `slotlock migrate`.
|
|
79
|
+
- `countResources` on the tenant store: how many resources a tenant has, archived ones included, as
|
|
80
|
+
a resource limit counts them.
|
|
81
|
+
- Usage metering, off unless `SLOTLOCK_USAGE=on` (RFC 0003 part 3). `slotlock.usage_daily` counts
|
|
82
|
+
each tenant's use by UTC day, API key and surface: every operation that reaches the backend (MCP,
|
|
83
|
+
A2A, REST, the application API, the dashboard assistant), reported by the agent server's new
|
|
84
|
+
`onUsage` and the dispatch's, counted in memory by `createSlotlockUsageMeter` and recorded every
|
|
85
|
+
15 seconds and on shutdown through `slotlock.record_usage`, once per batch id however often it is
|
|
86
|
+
sent (a batch it refuses for good, such as a day it no longer takes after a long outage, is
|
|
87
|
+
dropped and reported rather than held in front of the batches after it); and the bookings (`reservation.created`, `event.created`, `hold.confirmed`) and holds
|
|
88
|
+
(`hold.created`) committed on the tenant's feed, rolled up by maintenance
|
|
89
|
+
(`slotlock.roll_up_usage`, the store's `rollUpUsage`) before the feed is pruned, in the feed's
|
|
90
|
+
committed order, so nothing is counted twice and a rolled-back write never is; maintenance's own
|
|
91
|
+
changes are not counted, nor writes from before the release. Read with `slotlock.tenant_usage`
|
|
92
|
+
(`readSlotlockTenantUsage`). Kept 400 days, erased with the tenant; recording skips a tenant with
|
|
93
|
+
no key and no feed, so counts flushed after an erasure recreate nothing. The dashboard Worker
|
|
94
|
+
takes the same setting for the assistant's operations, recording each request's when it ends.
|
|
95
|
+
`serve` refuses to start with it on against a schema without the functions. The privacy notice
|
|
96
|
+
lists the counts. Run `slotlock migrate`.
|
|
97
|
+
- `GET /v1/usage` (`slotlock_get_usage`, admin scope): a tenant's usage from one UTC day to another,
|
|
98
|
+
by default the first of this month to today, at most 400 days: totals, then by day, by API key and
|
|
99
|
+
by surface, bounded whatever the volume; 400 `invalid_usage_range` for a range that is reversed,
|
|
100
|
+
too long or ends after today. In the OpenAPI document and the five clients. The store's
|
|
101
|
+
`readUsage` and usage.ts's `summarizeSlotlockUsage` back it.
|
|
102
|
+
- Usage also counts webhook deliveries an endpoint accepted (`slotlock serve`'s delivery pass), messages
|
|
103
|
+
sent to the dashboard assistant that it answered (the assistant's new `onMessage`), and each day's
|
|
104
|
+
resource count, archived ones included (maintenance's roll-up; a count, so `GET /v1/usage` totals
|
|
105
|
+
the last day's rather than summing). `GET /v1/usage` and the Plan page carry them, and the
|
|
106
|
+
privacy notice lists them; `slotlock.record_usage` takes the metrics a server counts and refuses
|
|
107
|
+
the ones the database does. The metric list's CHECK is replaced only when the list changes.
|
|
108
|
+
- Every answer of the agent server carries `Slotlock-Request-Id`, a UUID of its own; with
|
|
109
|
+
`onUsage` (`SLOTLOCK_USAGE=on`) an answer whose operations ran before it was sent also carries
|
|
110
|
+
`Slotlock-Usage: operations=<n>`, the operations counted for it, so a gateway can count on its side
|
|
111
|
+
and compare. The per-second `RateLimit` and `RateLimit-Policy` fields are as before.
|
|
112
|
+
- The dashboard's Plan page shows this month's operations, bookings and holds, and who used them by
|
|
113
|
+
API key, when the deployment meters (the dashboard's `usage` option, set from `SLOTLOCK_USAGE` by
|
|
114
|
+
`slotlock serve` and the Worker).
|
|
115
|
+
- A resource's profile (RFC 0003 G1, `resource-profile.ts`), each part optional: a `name` shown to
|
|
116
|
+
agents and people instead of the reference, a `description` (2,000 characters), a `kind`
|
|
117
|
+
(`person`, `room`, `desk`, `space`, `vehicle`, `machine`, `equipment`, `service`, `other`: a hint,
|
|
118
|
+
never a rule), a `capacity` (1-100,000, informational), up to 20 `labels` and `attributes` (up to
|
|
119
|
+
50 keys of text, numbers and booleans, 4 KiB). New columns on `slotlock.resources` with constant
|
|
120
|
+
defaults, so no row is rewritten, bounded by a CHECK the migration validates after it commits, and
|
|
121
|
+
a GIN index on labels. `createResource` takes `profile` (an existing reference keeps the fields
|
|
122
|
+
not given); the store's new `updateResourceProfile` changes the fields given, `null` clearing a
|
|
123
|
+
name, description or capacity; `listResources` narrows by `kind` and `labels` (every one).
|
|
124
|
+
`POST /v1/resources` takes the profile and `PATCH /v1/resources/{resource_id}`
|
|
125
|
+
(`slotlock_update_resource`) changes it, 400 `invalid_resource_profile` naming the `field`; a
|
|
126
|
+
resource in the application API carries all six. `slotlock_list_resources` shows agents each
|
|
127
|
+
resource's name, kind, capacity, labels and the first 500 characters of its description (never
|
|
128
|
+
its attributes), and narrows by `kind` and `labels`; a confirmation names a resource by its name.
|
|
129
|
+
`resource.updated` on the feed lists the profile fields that changed in `profile_changed`, never
|
|
130
|
+
their values. The dashboard's resource page edits the profile and shows the name in place of the
|
|
131
|
+
reference. The privacy notice lists names, descriptions, labels and attributes as calendar
|
|
132
|
+
content. In the OpenAPI document and the five clients. Run `slotlock migrate`.
|
|
133
|
+
- Locations and groups (RFC 0003 G2, G3). A location is a place in a tree of the tenant's own
|
|
134
|
+
(`slotlock.locations`: at most 5 levels and 1,000 a tenant, held by a guard trigger under the
|
|
135
|
+
tenant's lock, which also refuses a location under itself), and a resource is at one at most
|
|
136
|
+
(`slotlock.resources.location_ref`, a foreign key added NOT VALID). A group is any set of the
|
|
137
|
+
tenant's resources (`slotlock.resource_groups` and `resource_group_members`: 500 a tenant, 20 a
|
|
138
|
+
resource), with no booking rule; a pool stays how resources are booked as a class. All three are
|
|
139
|
+
tenant tables under forced row-level security, erased with the tenant. A resource created at a
|
|
140
|
+
location without a time zone takes the nearest one its location, or one above it, has. The store
|
|
141
|
+
gains `listLocations`, `putLocation`, `updateLocation`, `deleteLocation` (`location_in_use` while
|
|
142
|
+
a resource is at it or a location is in it), `listGroups`, `putGroup`, `deleteGroup`,
|
|
143
|
+
`addGroupMember` and `removeGroupMember`; `createResource` and `updateResourceProfile` take a
|
|
144
|
+
`location`, and `listResources` narrows by `location` (with every location under it) and
|
|
145
|
+
`group`. The application API gains `GET`/`POST /v1/locations`, `PATCH`/`DELETE
|
|
146
|
+
/v1/locations/{location_ref}`, `GET`/`POST /v1/groups`, `DELETE /v1/groups/{group_ref}` and
|
|
147
|
+
`PUT`/`DELETE /v1/groups/{group_ref}/members/{resource_id}`; a resource carries its `location` and
|
|
148
|
+
`groups`, `POST /v1/resources` no longer defaults `timezone` in its schema (the store still gives
|
|
149
|
+
UTC), and `PATCH /v1/resources/{resource_id}` moves it. `slotlock_list_resources` shows agents each
|
|
150
|
+
resource's location (with its name) and groups, and narrows by `location` and `group`, with or
|
|
151
|
+
without a pool. `resource.created` and `resource.updated` carry the location. Run `slotlock
|
|
152
|
+
migrate`.
|
|
153
|
+
- Partner accounts (RFC 0003 P1-P4, T2). A partner reselling Slotlock runs a tenant for each of
|
|
154
|
+
its users, `partner:<partner>:<account>`. The schema owner makes partners with a contract (the
|
|
155
|
+
plans their accounts may be on, with limits and monthly quotas, the scopes their keys may carry,
|
|
156
|
+
and prices) and their keys: `slotlock partner create|list|suspend|unsuspend|key|revoke-key|
|
|
157
|
+
customer|webhook`. A partner key (`slk_partner_…`, a SHA-256 kept) carries the new `partner`
|
|
158
|
+
scope, which reaches only the eleven partner operations under `/v1/partner` (a new `Partner`
|
|
159
|
+
OpenAPI tag, `x-slotlock-scope: partner`, in the five clients): create an account or change its
|
|
160
|
+
plan, list and read accounts, make, list, rotate and revoke their keys, suspend and lift its own
|
|
161
|
+
suspension (never the deployment's), erase an account, and read its accounts' usage. An
|
|
162
|
+
application operation may now name its scope (`scope: 'partner'`); a principal carries
|
|
163
|
+
`partnerId`. New owner-only tables `slotlock.partners`, `partner_keys`, `partner_accounts` and
|
|
164
|
+
`partner_invoices`, and functions `partner_tenant_blocked`, `authenticate_partner_key`,
|
|
165
|
+
`partner_contract`, `put_partner_account`, `list_partner_accounts`, `get_partner_account`,
|
|
166
|
+
`set_partner_account_suspended` and `partner_usage`. `tenant_plans` gains `source`,
|
|
167
|
+
`partner_id` and the account's own quotas (`slotlock.tenant_plan_quotas`), and its plan check
|
|
168
|
+
takes a contract's plan names for a partner's account (added NOT VALID); quotas read a tenant's
|
|
169
|
+
own numbers before its plan's (`readSlotlockTenantQuotas`; `createSlotlockQuotas`'s `plan`
|
|
170
|
+
returns quota numbers). A suspended partner suspends every account (`tenant_suspended`) and
|
|
171
|
+
their keys stop authenticating (`authenticate_scoped_api_key`); erasing an account removes the
|
|
172
|
+
partner's record of it. Run `slotlock migrate`.
|
|
173
|
+
- Tracing and metrics to OpenTelemetry: a call's span is now a child of the caller's span (given to
|
|
174
|
+
the tracer alone through the agent server's new `onCallTrace`; the events and logs still never
|
|
175
|
+
carry it), and the metrics are exported over OTLP each minute (`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`,
|
|
176
|
+
or `<endpoint>/v1/metrics`; `createSlotlockOtlpMetricsExporter`, `SlotlockMetrics.collect`).
|
|
177
|
+
- The five clients take an account for a partner key to act for (`Slotlock-Account`): `account=`
|
|
178
|
+
(Python), `WithAccount` (Go), `Options.account` (Rust), `.account(…)` (Java),
|
|
179
|
+
`SlotlockClientOptions.Account` (.NET), checked before any request.
|
|
180
|
+
- Partner reconciliation and a daily billing job (RFC 0003 P5, P6): `slotlock_partner_reconcile_usage`
|
|
181
|
+
(`POST /v1/partner/usage/reconcile`) compares a partner's counts of an ended day with Slotlock's,
|
|
182
|
+
account by account, and records a day off by more than 0.5% in the new owner-only table
|
|
183
|
+
`slotlock.partner_reconciliations`. `slotlock billing daily`, for a daily cron job as the schema
|
|
184
|
+
owner, closes last month for every active partner once it has been over for 6 hours, reports
|
|
185
|
+
overage when it is on, and lists the week's mismatches, exiting 1 when anything needs attention.
|
|
186
|
+
Month close freezes the month (`slotlock.partner_month_closes`), bills erased accounts from
|
|
187
|
+
`slotlock.partner_erased_usage`, resends the webhook until it is taken, and adopts a Stripe item
|
|
188
|
+
an interrupted run made. The partner key calls take `environment: test`. Run `slotlock migrate`.
|
|
189
|
+
- Metered overage for Developer (RFC 0003 Later), off by default: with `SLOTLOCK_OVERAGE=on` (or a
|
|
190
|
+
stop percent, 111-1000) and `SLOTLOCK_QUOTAS=on`, a paying Developer tenant is refused only past
|
|
191
|
+
that share of its monthly operations (`readSlotlockTenantQuotas`'s `overageStopPercent`); the
|
|
192
|
+
Worker adds `SLOTLOCK_STRIPE_PRICE_OVERAGE`, a metered price, to a Developer checkout and shows
|
|
193
|
+
`SLOTLOCK_PLAN_PRICE_OVERAGE`; and `slotlock overage report [--month] [--event]`, as the schema
|
|
194
|
+
owner with `STRIPE_SECRET_KEY`, sends Stripe Billing Meter events for what each tenant used above
|
|
195
|
+
its quota and has not been billed for, recorded first in the new owner-only table
|
|
196
|
+
`slotlock.overage_reports` (erased with the tenant), so nothing is billed twice
|
|
197
|
+
(`reportSlotlockOverage`, `overage.ts`); an event Stripe refuses as already taken (its answer to a
|
|
198
|
+
resent identifier) counts as sent, so a lost answer neither sticks nor bills twice. Stripe's test
|
|
199
|
+
mode takes the metered price in a Managed Payments checkout; it stays off until a paid test
|
|
200
|
+
subscription shows the meter billed and the Terms name the price. Run `slotlock migrate`.
|
|
201
|
+
- OpenTelemetry trace export and published objectives (RFC 0002 P2-5, RFC 0003 Later). With
|
|
202
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`), `serve` sends a SERVER
|
|
203
|
+
span for each MCP, A2A and REST call to an OpenTelemetry collector, OTLP over HTTP as JSON, in
|
|
204
|
+
batches from a bounded queue (a collector that is down drops spans, never a call): its protocol,
|
|
205
|
+
operation, outcome or HTTP status and duration, in the caller's trace when it sent a
|
|
206
|
+
`traceparent`, naming no tenant, key, argument or address. `OTEL_EXPORTER_OTLP_HEADERS` and
|
|
207
|
+
`OTEL_SERVICE_NAME` as the standard says. `createSlotlockOtlpExporter` and `slotlockCallSpan`
|
|
208
|
+
(`otel.ts`). The status page publishes the hosted service's objectives (99.5% of calls succeed,
|
|
209
|
+
95% under a second, 99.5% of probe checks up), not an SLA, with Prometheus recording and
|
|
210
|
+
multiwindow burn-rate alerting rules in `docs/hosted/slo-rules.yml`.
|
|
211
|
+
- Composite resources (RFC 0003 G8, T2): a resource can be a part of another
|
|
212
|
+
(`slotlock_set_resource_parent`, `PUT /v1/resources/{resource_id}/parent`, `setResourceParent`,
|
|
213
|
+
`slotlock resource parent <ref> <parent-ref | none>`), one level deep, at most 50 parts. Each
|
|
214
|
+
booking or hold on a part or a whole claims its place in the new tenant table
|
|
215
|
+
`slotlock.composite_claims` (forced RLS), kept apart by `EXCLUDE (resource_id WITH =, during
|
|
216
|
+
WITH &&, span WITH &&)`: the whole conflicts with every part, two parts never. A trigger on
|
|
217
|
+
reservations keeps the claims; reaping an expired hold covers the composite's other resources;
|
|
218
|
+
free/busy and the next-free search read the composite's other bookings. `resources` gains
|
|
219
|
+
`parent_id` and `part_index` (the foreign key and check added NOT VALID); the application API's
|
|
220
|
+
resource gains `parent_id` and `part_index`, and `slotlock_list_resources` shows `part_of`. A
|
|
221
|
+
composite changes only while neither side has a booking or live hold to come
|
|
222
|
+
(`composite_busy`). A booking refused by the composite names the booking in its way; the feed's
|
|
223
|
+
`resource.updated` carries `parent` before and after; the dashboard's resource page has a
|
|
224
|
+
Composite panel. Run `slotlock migrate`.
|
|
225
|
+
- Sign-in with Google (OpenID Connect, RFC 0003 Later, T2), with `SLOTLOCK_GOOGLE_CLIENT_ID` and
|
|
226
|
+
`SLOTLOCK_GOOGLE_CLIENT_SECRET`: Continue with Google on the sign-in page (`/sign-in?with=google`,
|
|
227
|
+
back to the same callback), `openid email` with `state`, PKCE and a nonce; the ID token from
|
|
228
|
+
Google's token endpoint is checked for issuer, audience (and `azp`), expiry and nonce. Each
|
|
229
|
+
person is their own tenant, `google:<subject>`; `SLOTLOCK_DASHBOARD_USERS` takes
|
|
230
|
+
`google:<subject>` entries, and `serve` refuses a Google tenant's keys unless the list names it
|
|
231
|
+
(or is `*`). Not with a list of `SLOTLOCK_DASHBOARD_ORGS`. Erasing a Google person's tenant
|
|
232
|
+
removes their dashboard form records. The ID token's RS256 signature is checked against Google's
|
|
233
|
+
published keys (`google.jwksUrl`, kept for the answer's max-age), through WebCrypto. The
|
|
234
|
+
dashboard's `google` option.
|
|
235
|
+
- The Team plan, an organization's (Teams on Cloud): $40 a month or $400 a year per organization,
|
|
236
|
+
500 resources, 100 keys and 25 webhook endpoints, with 2,000,000 operations and 100,000 bookings
|
|
237
|
+
a month, stopping at 110%. The dashboard's resource cap rises from 100 to 500 and a tenant's
|
|
238
|
+
webhook endpoints from 10 to 25 (`SLOTLOCK_DASHBOARD_MAX_RESOURCES`,
|
|
239
|
+
`SLOTLOCK_MAX_WEBHOOK_ENDPOINTS`); a plan's own limits stay lower. The Terms and the hosted page
|
|
240
|
+
name it. The Worker offers it to an organization's owner once `SLOTLOCK_STRIPE_PRICE_TEAM_MONTHLY`
|
|
241
|
+
and `SLOTLOCK_STRIPE_PRICE_TEAM_YEARLY` are set (the prices shown default to $40 and $400); a
|
|
242
|
+
subscription at a Team price gives Team (`slotlockPlanOfSubscription`'s `teamPrices`, the
|
|
243
|
+
Stripe client's `planOf`). `tenant_plans` takes `team` (its source check is replaced, NOT
|
|
244
|
+
VALID). `SlotlockPlan` gains `team`; the dashboard's billing gains `teamPrices` and checkout a
|
|
245
|
+
`plan`. Run `slotlock migrate`.
|
|
246
|
+
- Teams on Cloud (RFC 0003, T2): `SLOTLOCK_DASHBOARD_ORGS=enabled` gives every person their own
|
|
247
|
+
tenant and the GitHub organizations they belong to that an owner turned on, from the dashboard's
|
|
248
|
+
new Teams page (`POST /teams/enable`, `/teams/disable`, the latter confirmed with the
|
|
249
|
+
organization's name), by an owner who signed in within the last 15 minutes. The workspace menu switches between them; a session in an organization
|
|
250
|
+
turned off is back in its own tenant at its next request, and `serve` refuses the
|
|
251
|
+
organization's keys (checked in the database, kept 30 seconds: `createSlotlockOrganizationCheck`).
|
|
252
|
+
New owner-only table `slotlock.dashboard_organizations` with `enabled_organizations`,
|
|
253
|
+
`enable_organization` and `disable_organization`, granted by `slotlock migrate`; erasing an
|
|
254
|
+
organization's tenant removes its row. `createSlotlockDashboardTeams` (teams.ts), the dashboard's
|
|
255
|
+
`teams` option. Run `slotlock migrate`.
|
|
256
|
+
- Acting for a partner's account (RFC 0003 P3, T2): a partner key with a `Slotlock-Account` field
|
|
257
|
+
runs the request as that account, with the contract's key scopes, where the contract allows it
|
|
258
|
+
(`act_on_behalf`, off by default). The principal carries `onBehalfOf`, the history records the
|
|
259
|
+
partner key as the actor, and serve refuses the field with any other credential.
|
|
260
|
+
`SLOTLOCK_ACCOUNT_FIELD` in `partners.ts`.
|
|
261
|
+
- Test environments (RFC 0003 G9, T2). A tenant reference ending `~test` is the test environment of
|
|
262
|
+
the tenant before it: a tenant of its own, on its live tenant's plan and quotas, never billed
|
|
263
|
+
(`tenant_plan` returns no Stripe ids for it), suspended with its live tenant and erased with it
|
|
264
|
+
(`erase_tenant` reports its rows as `test.<table>`). Its keys start `slk_test_` and only its keys
|
|
265
|
+
do: `slotlock_api_keys_environment_valid` (added NOT VALID) and a widened prefix check.
|
|
266
|
+
`isSlotlockApiKey` takes both forms; `generateSlotlockApiKey(test)`, `isSlotlockTestTenant`,
|
|
267
|
+
`slotlockLiveTenant`, `SLOTLOCK_TEST_API_KEY_PREFIX`. The dashboard has a Live/Test switch
|
|
268
|
+
(`POST /environment`, kept in the session) with a banner in Test, where plan and billing actions
|
|
269
|
+
are refused. `slotlockServeAllowsTenant` judges a test environment as its live tenant, so a
|
|
270
|
+
person removed from `SLOTLOCK_DASHBOARD_USERS` loses their test keys too. Run `slotlock migrate`.
|
|
271
|
+
- Closing a partner's month (RFC 0003 P5, P6): `slotlock partner close <id> [--month YYYY-MM]`,
|
|
272
|
+
as the schema owner, closes an ended month: it prices each account's usage with the contract
|
|
273
|
+
(`priceSlotlockPartnerMonth`), records the lines once in `slotlock.partner_invoices`, sends the
|
|
274
|
+
signed `usage.period_closed` webhook (the totals by account and metric, the lines and a SHA-256,
|
|
275
|
+
`slotlockPartnerMonthChecksum`), and with `STRIPE_SECRET_KEY` adds each line Stripe has not
|
|
276
|
+
taken to the partner's next invoice; it is safe to run again (`closeSlotlockPartnerMonth` in
|
|
277
|
+
`partner-billing.ts`). `/metrics` gains `slotlock_requests_by_client_total{client}`, from the
|
|
278
|
+
`Slotlock-Client` request field matched against the new `SLOTLOCK_KNOWN_CLIENTS`.
|
|
279
|
+
- Keys limited to part of their tenant (RFC 0003 G6). `slotlock key create --locations <refs>
|
|
280
|
+
--groups <refs>`, the dashboard's "Limit to locations or groups" (Developer plan and up when the
|
|
281
|
+
dashboard has plans) or the key store's `resourceScope` makes a key that reaches only the
|
|
282
|
+
resources at those locations (each with every location in it) or in those groups, up to 20 of
|
|
283
|
+
each, read and write only, for good (kept through rotation). The database enforces it: withTenant's
|
|
284
|
+
new `resourceScope` sets `slotlock.resource_scope` to the key's resources for the transaction, and
|
|
285
|
+
a restrictive `resource_scope_v1` policy on every table keyed by resource or by event narrows each
|
|
286
|
+
read and write to them, so a resource outside is not found (`resource_not_found`, never
|
|
287
|
+
`forbidden`) and the feed carries only its resources' changes. New owner-only
|
|
288
|
+
`slotlock.api_key_resource_scopes` (erased with the key) and definer functions
|
|
289
|
+
`set_api_key_resource_scope`, `authenticate_scoped_api_key` and `list_api_key_resource_scopes`;
|
|
290
|
+
a principal carries `resourceScope`, which the store backends pass on; key lists show it. The
|
|
291
|
+
deployment's RLS check accepts the restrictive policy beside the tenant one, and nothing else. Run
|
|
292
|
+
`slotlock migrate`, then grant the application role again (`slotlock migrate` does).
|
|
293
|
+
- Group membership changes are on the feed. A resource joining or leaving a group, a group's
|
|
294
|
+
deletion included, appends `resource.updated` with the groups it was in `before` and is in
|
|
295
|
+
`after`, by reference, sorted: one change for each resource a statement touched (a statement
|
|
296
|
+
trigger over its transition table on `slotlock.resource_group_members`), none for adding it to a
|
|
297
|
+
group it is in, for the deletion of the resource itself (`resource.deleted` says so) or for
|
|
298
|
+
erasure. Webhooks deliver it, and a webhook filtered by a group matches the resource as it is when
|
|
299
|
+
the change is delivered, as every filter does. Run `slotlock migrate`.
|
|
300
|
+
- The dashboard edits locations and groups. Its new Locations and groups page, linked from
|
|
301
|
+
Resources, adds a location (with where it is and a time zone), moves, renames and deletes one, and
|
|
302
|
+
adds, renames and deletes a group, each refusal said in a person's words. A resource's page puts
|
|
303
|
+
it at a location and in its groups in one form, in one transaction, so a group it may not join
|
|
304
|
+
moves nothing. Resources adapters gain the optional `grouping`, which
|
|
305
|
+
`createSlotlockDashboardResources` provides; without it the page and the panel are not shown.
|
|
306
|
+
- Search by selection (RFC 0003 G4). `slotlock_get_free_busy`, `slotlock_find_next_available` and
|
|
307
|
+
`slotlock_find_available_slots` (and their REST routes) take `location` (with every location in
|
|
308
|
+
it), `group`, `labels` (every one), `kind` or `pool` in place of `resource_ids`, one or the other.
|
|
309
|
+
The agent server resolves the selection through the backend's new optional `selectResources` (the
|
|
310
|
+
shared, unarchived resources that match, by id, at most 100, or 20 for slots), runs the search on
|
|
311
|
+
them and names them in `selected_resource_ids`; more than the limit is refused whole with `400
|
|
312
|
+
selection_too_large`, none with `404 selection_empty`, and a backend without `selectResources`
|
|
313
|
+
answers `400 selection_not_supported`. The store's `listResources` also narrows by `pool`.
|
|
314
|
+
- Webhook filters (RFC 0003 G5). `POST /v1/webhooks` takes `resource_ids` (up to 100), `pools`,
|
|
315
|
+
`locations` and `groups` (up to 20 each): the endpoint then receives only changes about a resource
|
|
316
|
+
that matches one of them, as it is when the change is delivered, a location matching every
|
|
317
|
+
location in it, and no change about no resource. New columns on `slotlock.webhook_endpoints`
|
|
318
|
+
(bounded by a CHECK added NOT VALID), and `slotlock.location_path(text, text)`. The store's
|
|
319
|
+
`createWebhookEndpoint` takes a `filter` (`invalid_webhook_filter` past its bounds), an endpoint
|
|
320
|
+
carries its `filter`, `webhook.created` on the feed records it, and the dashboard's webhooks page
|
|
321
|
+
shows it. A change that says where its resource was also matches: `resource.deleted` (now with the
|
|
322
|
+
resource's `pool`, `location` and `groups`, recorded by a BEFORE DELETE trigger while its groups
|
|
323
|
+
are still there) and `resource.updated` by its `before`, so a filtered endpoint learns of a
|
|
324
|
+
resource deleted or moved out of what it follows. An endpoint that takes only some changes (by
|
|
325
|
+
type or filter) moves its cursor past those it does not take, up to the last change that has
|
|
326
|
+
ended, when none before it is for it: it no longer keeps the tenant's whole feed from being
|
|
327
|
+
pruned. This covers `event_types` filters too, which held it back the same way.
|
|
328
|
+
- Pool order (RFC 0003 G7). `PUT /v1/pools/{pool}/order` (`slotlock_set_pool_order`,
|
|
329
|
+
`setPoolOrder`) sets the order `slotlock_book_pool` tries a pool's members in: `stable` (by id, as
|
|
330
|
+
before, and the default), `least_booked` (fewest confirmed bookings made in the last 30 days
|
|
331
|
+
first) or `round_robin` (starting after the member it booked last). `GET /v1/pools` shows each
|
|
332
|
+
pool's `order`. New tenant table `slotlock.pool_settings`, under the tenant's forced row-level
|
|
333
|
+
security and erased with it, for at most 1,000 pools a tenant (`pool_order_limit_reached`, 409,
|
|
334
|
+
for one more); an unknown order is refused `invalid_pool_order`. Run `slotlock migrate`.
|
|
335
|
+
- Provider presets and a model list in the assistant's Settings (RFC 0003 A4). Anthropic, OpenAI,
|
|
336
|
+
OpenRouter, Google, Mistral, Groq, Together and Custom each fill the API and base URL, saving
|
|
337
|
+
nothing; **Load models** asks the provider for its models (`GET /v1/models` on the Anthropic API,
|
|
338
|
+
`GET /models` on an OpenAI-compatible one, listed under `data` or, as Together answers, as a bare
|
|
339
|
+
array) and offers them for the Model field. It checks the URL
|
|
340
|
+
as saving does, sends the saved key only to the provider and URL it was saved for, never follows a
|
|
341
|
+
redirect, and counts against the member's message rate. `SLOTLOCK_ASSISTANT_PRESETS` and
|
|
342
|
+
`listSlotlockAssistantModels` in `assistant.ts`.
|
|
343
|
+
- Monthly quotas, off unless `SLOTLOCK_QUOTAS=on` (which needs `SLOTLOCK_USAGE=on`; RFC 0003
|
|
344
|
+
part 3). A tenant on a plan may run so many operations and make so many bookings a UTC calendar
|
|
345
|
+
month (`SLOTLOCK_PLAN_QUOTAS`): Free 25,000 and 1,000, refused at 100%; Developer 500,000 and
|
|
346
|
+
25,000, refused at 110%. Past the operations quota every operation is refused but
|
|
347
|
+
`slotlock_get_usage` and `slotlock_erase_tenant` (`SLOTLOCK_QUOTA_EXEMPT_OPERATIONS`), past the
|
|
348
|
+
bookings quota only those that can book (`SLOTLOCK_BOOKING_OPERATIONS`), so reads and
|
|
349
|
+
cancellations still run; the refusal is `429 quota_exceeded`, naming the metric, the quota, the stop, the count and
|
|
350
|
+
the reset, with `Retry-After` at the month's end. Every operation's answer lists the quotas in
|
|
351
|
+
its `RateLimit` and `RateLimit-Policy` fields (`"operations_month"`, `"bookings_month"`, counting
|
|
352
|
+
down to the stop), after the per-second bucket. `createSlotlockQuotas` reads the month so far
|
|
353
|
+
through `slotlock.tenant_quota_usage` (bookings not yet rolled up included) at most every 30
|
|
354
|
+
seconds, every 2 near a stop, and adds this server's unrecorded operations (the meter's new
|
|
355
|
+
`local`); a count it cannot read lets the operation run. Booking operations let run since a read
|
|
356
|
+
are counted as attempts, so before refusing one on that estimate a server reads the count again. At 80% and 100% of a quota the tenant's
|
|
357
|
+
feed records the new `usage.threshold_reached` change once, bookings as the database counted them, (`slotlock.note_usage_threshold`,
|
|
358
|
+
`slotlock.usage_notices`, erased with the tenant), which its webhooks deliver. The request limits
|
|
359
|
+
take `quotas` and a `quota` check for `authorize`; the dashboard assistant is checked when an
|
|
360
|
+
operation runs, never when one is proposed, on `slotlock serve` and the Worker. The dashboard's
|
|
361
|
+
`quotas` option shows the Plan page's use against them and warns from 80%. `/metrics` and the
|
|
362
|
+
refusal log count `quota_exceeded`. The Terms and the hosted service page name the quotas from 10
|
|
363
|
+
December 2026; the privacy notice lists the notices. Run `slotlock migrate`.
|
|
364
|
+
- `SLOTLOCK_ASSISTANT_PREVIOUS_SECRETS`: up to three earlier values of `SLOTLOCK_ASSISTANT_SECRET`,
|
|
365
|
+
comma-separated, newest first, for `slotlock serve` and the dashboard Worker. A model key sealed
|
|
366
|
+
under one of them still opens, and is sealed again under the current secret the next time its
|
|
367
|
+
organization uses the assistant, by `resealAssistantKey` on the tenant store, which replaces the
|
|
368
|
+
stored seal only while it is still the one that was read, so a key an owner saved meanwhile is
|
|
369
|
+
never overwritten. Each must be as strong as a secret, and differ from the others, from the
|
|
370
|
+
current secret and from every other secret; the log redacts each one. Changing the assistant
|
|
371
|
+
secret no longer makes owners enter their keys again.
|
|
372
|
+
- `SLOTLOCK_DASHBOARD_TERMS_URL` and `SLOTLOCK_DASHBOARD_PRIVACY_URL`: with an https URL set, the
|
|
373
|
+
dashboard's sign-in page names the terms a person accepts by continuing, and links the privacy
|
|
374
|
+
notice. Without them it names no terms, as before.
|
|
375
|
+
- Docs: a Use cases page, on how to model car and van rental, equipment hire, clinics, courts,
|
|
376
|
+
garage service bays, salons, meeting rooms and field service, and where Slotlock does not fit.
|
|
377
|
+
|
|
378
|
+
### Changed
|
|
379
|
+
|
|
380
|
+
- Rust client: every write's input struct has a `dry_run: Option<bool>` field (RFC 0004 A3), and
|
|
381
|
+
`PartnerCreateKeyInput` a `limits` field (A5). A struct literal that lists every field must now
|
|
382
|
+
end with `..Default::default()`, as the README's examples do; the other clients take them as
|
|
383
|
+
optional arguments.
|
|
384
|
+
- `resource_ids` is optional on `slotlock_get_free_busy`, `slotlock_find_next_available` and
|
|
385
|
+
`slotlock_find_available_slots`, since a selection can stand in for it. Calls written before
|
|
386
|
+
still compile in every client: Rust keeps the field a `Vec<String>` (left out when empty), Java
|
|
387
|
+
keeps the methods that take it first beside new ones that take it in `optional`, and .NET keeps
|
|
388
|
+
its 0.2.0 overloads (the generator now keeps a parameter that became optional, which
|
|
389
|
+
`RELAXED_ARGUMENTS` in `clients/generate.ts` lists). A Rust caller that names every field of an
|
|
390
|
+
input struct, rather than ending with `..Default::default()`, gains new fields to name.
|
|
391
|
+
|
|
392
|
+
- The dashboard Worker no longer reads `SLOTLOCK_FREE_PLAN_FROM` and answers 503 while it is set,
|
|
393
|
+
naming `slotlock plan default` instead: the date is the database's now.
|
|
394
|
+
- Erasing a tenant whose plan names a live Stripe subscription is refused with `billing_active`
|
|
395
|
+
(`slotlock tenant erase` exits 1; `/v1/tenant/erase` answers 409): cancel the subscription, whose
|
|
396
|
+
webhook moves the tenant to Free, then erase. The privacy notice already promised it.
|
|
397
|
+
- Upgrade on the Plan page opens no second Checkout for a customer that already has an active,
|
|
398
|
+
trialing or past-due subscription, and points it to Manage billing instead.
|
|
399
|
+
- A Stripe webhook about a subscription that ended no longer moves its tenant to Free while another
|
|
400
|
+
of the tenant's subscriptions still renews (two Checkouts paid in two tabs): the plan names the
|
|
401
|
+
live one. A second live subscription is logged as `slotlock billing duplicate subscription`, by
|
|
402
|
+
Stripe ids only, so it can be refunded.
|
|
403
|
+
- Stripe requests send `Stripe-Version: 2026-04-22.dahlia` (`SLOTLOCK_STRIPE_API_VERSION`) unless
|
|
404
|
+
`STRIPE_API_VERSION` is set, rather than the account's default: Managed Payments needs
|
|
405
|
+
2025-03-31.basil or later.
|
|
406
|
+
- The Plan page counts resources as the plan does, archived ones included; the Resources page says
|
|
407
|
+
"*n* of *limit* on your plan" with the plan's limit, not the dashboard's 100, and a refused add,
|
|
408
|
+
key or webhook names the plan.
|
|
409
|
+
- Talk to the founder defaults to the founder's Pylota address, on the Plan page, the landing page
|
|
410
|
+
and the hosted service page.
|
|
411
|
+
- The hosted service page has an Assistant section: bring your own model (Anthropic or
|
|
412
|
+
OpenAI-compatible), what is sent to it, that writes wait for approval, its bounds, and that a model
|
|
413
|
+
must be reachable from the internet. A test holds it to the bounds in the code. The Worker's
|
|
414
|
+
wrangler.jsonc says to set `SLOTLOCK_ASSISTANT_SECRET` once and keep it.
|
|
415
|
+
- Site: the landing page leads with what agents can book and with Slotlock Cloud, in under 1,300
|
|
416
|
+
words instead of 2,607 and 12 bands instead of 15. It shows the dashboard in its first screen,
|
|
417
|
+
says Pylota checks every booking with Slotlock, lists the industries it serves, and has a pricing
|
|
418
|
+
table: Free, Developer, Team and Self-host, each hosted plan's limits and monthly allowances read
|
|
419
|
+
from `src/plans.ts` and its price and the Custom contact from the Plans table in
|
|
420
|
+
`docs/hosted/README.md`, so the page cannot offer a plan the code does not sell. Running it
|
|
421
|
+
yourself now lives in Pricing, a Source band and the docs; the hero's install card moved to the
|
|
422
|
+
developers band. Copy lint fails the build above 1,300 words.
|
|
423
|
+
- Privacy notice: email sent to support@, abuse@ or privacy@pylota.io is held in Google Workspace for
|
|
424
|
+
two years; Google is listed as a recipient, with its transfers. The notice has no owner TODO left.
|
|
425
|
+
- README: about 160 lines that say what Slotlock is, how to run it and where each topic is
|
|
426
|
+
documented, down from 1,659. Everything it explained is on
|
|
427
|
+
[slotlock.pylota.io/docs](https://slotlock.pylota.io/docs/), which gains pages for hours and
|
|
428
|
+
booking rules, limits, the dashboard on Cloudflare Workers, recurring events and external
|
|
429
|
+
calendars, and now holds the store guide, organizations, the assistant's limits and the protocol
|
|
430
|
+
reference in full. A link to a README section from an earlier release lands on the README, whose
|
|
431
|
+
Documentation table names each topic's page.
|
|
432
|
+
|
|
433
|
+
### Fixed
|
|
434
|
+
|
|
435
|
+
- Docs: the self-hosting page said `SLOTLOCK_TRUSTED_PROXY_HEADER` is ignored without
|
|
436
|
+
`SLOTLOCK_TRUSTED_PROXY_SECRET`. The header is believed whenever no secret is set; the secret
|
|
437
|
+
limits it to requests that carry the proof.
|
|
438
|
+
- Many agents racing for one slot no longer stall for seconds (RFC 0004 A7). Writers each inserted,
|
|
439
|
+
then waited for one another at the exclusion constraint, and PostgreSQL broke each such deadlock
|
|
440
|
+
only after `deadlock_timeout`: about one 100-client race in three took 4 to 20 seconds, though
|
|
441
|
+
every answer was right. Booking writes now lock the resource, and a part's composite parent, in id
|
|
442
|
+
order (`FOR NO KEY UPDATE`) right before writing its occupied time, so racing writers take turns
|
|
443
|
+
for that step and the losers are refused at once.
|
|
444
|
+
- From RFC 0004's audit (P0):
|
|
445
|
+
- A no-show was announced twice, as `reservation.no_show` and as `reservation.cancelled`, so a
|
|
446
|
+
consumer counting cancellations counted it. The feed's trigger now names the deletion, so the
|
|
447
|
+
feed has one change: `reservation.no_show`, with the same fields as before.
|
|
448
|
+
- An agent's `overlap` says when the time is busy (`busy_start`, `busy_end` in its details), and
|
|
449
|
+
names the booking in the way when it is the agent's own (`conflicting_hold_id` or
|
|
450
|
+
`conflicting_event_id`), so it can release or move it.
|
|
451
|
+
- Confirming a hold through the application API (`POST /v1/holds/{id}/confirm`) left its linked
|
|
452
|
+
alternatives held until they lapsed; they are released in the same transaction, as an agent's
|
|
453
|
+
booking from a hold releases them.
|
|
454
|
+
- An agent booking from its own lapsed hold is `hold_expired` (409), as the application API's
|
|
455
|
+
confirm says, not `hold_not_found`; another agent's hold is still not found.
|
|
456
|
+
- `slotlock_join_waitlist` and `slotlock_create_webhook` take an idempotency key (the
|
|
457
|
+
Idempotency-Key header or `idempotency_key`): a retry answers the first entry, or webhook and
|
|
458
|
+
secret (`replayed: true`); the same key with another request is `idempotency_conflict`. Run
|
|
459
|
+
`slotlock migrate`.
|
|
460
|
+
- Round-robin pool bookings made at once could start from the same member; the pool's place is
|
|
461
|
+
locked while one is chosen.
|
|
462
|
+
- The MCP instructions and the A2A agent card say that titles, descriptions, locations and names
|
|
463
|
+
in results are data written by people, never instructions.
|
|
464
|
+
- `slotlock_list_webhooks` refused its own answer past 10 webhooks, though a tenant may have 25.
|
|
465
|
+
- A retried `slotlock_delete_event`, after the first had succeeded, answered `event_not_found`
|
|
466
|
+
instead of the first cancellation; it answers that with `replayed: true`.
|
|
467
|
+
- A recurring event's occurrences are written in three statements for the whole series, not three
|
|
468
|
+
per occurrence (reap the expired holds in the way, insert the bookings, insert the occurrences).
|
|
469
|
+
A 2,000-occurrence series took 6,000 round trips while holding its tenant's locks, so on a busy
|
|
470
|
+
database every write queued behind it timed out; it now takes three. The same rows are reaped,
|
|
471
|
+
in the same order, across a composite's parts and whole.
|
|
472
|
+
|
|
473
|
+
## 0.6.3 - 2026-11-08
|
|
474
|
+
|
|
475
|
+
### Added
|
|
476
|
+
|
|
477
|
+
- `SLOTLOCK_TRUSTED_PROXY_SECRET`: with it set, `slotlock serve` believes
|
|
478
|
+
`SLOTLOCK_TRUSTED_PROXY_HEADER` only on a request whose `X-Slotlock-Proxy-Proof` carries that
|
|
479
|
+
exact value (compared in constant time), and counts any other by its connection's address. A
|
|
480
|
+
process that reaches the server without passing the proxy, such as another container on
|
|
481
|
+
kamal-proxy's network, can then no longer pick its own rate-limit bucket. Unset, nothing changes.
|
|
482
|
+
The `listening` line says `client_address_proof`.
|
|
483
|
+
|
|
484
|
+
### Changed
|
|
485
|
+
|
|
486
|
+
- Site: the landing page's Slotlock Cloud endpoints list only what an agent connects to (MCP, A2A,
|
|
487
|
+
REST, OpenAPI, the agent card). The dashboard row is gone: people reach it through Sign in.
|
|
488
|
+
|
|
489
|
+
### Fixed
|
|
490
|
+
|
|
491
|
+
- `slotlock serve` maintains every tenant with data every hour, not only the ones whose keys it has
|
|
492
|
+
served since it started: a tenant gone quiet has its expired holds, ended waitlist entries and
|
|
493
|
+
feed past retention deleted too. Tenants are listed by name in `slotlock.maintenance_tenants`
|
|
494
|
+
(owner only, filled by triggers as a tenant first writes, and from existing rows when `migrate`
|
|
495
|
+
creates it); the serving role reads the names through `slotlock.maintenance_tenants(text)` and
|
|
496
|
+
maintains each in its own tenant context. Run `slotlock migrate`.
|
|
497
|
+
|
|
6
498
|
## 0.6.2 - 2026-11-08
|
|
7
499
|
|
|
8
500
|
### Changed
|
package/CONTRIBUTING.md
CHANGED
|
@@ -51,11 +51,18 @@ A check on each pull request fails when a commit lacks the line. To fix earlier
|
|
|
51
51
|
|
|
52
52
|
## Documentation
|
|
53
53
|
|
|
54
|
-
Every TypeScript
|
|
55
|
-
(`// #region <name>` … `// #endregion <name>`)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`
|
|
54
|
+
Every TypeScript example the docs show is a region of `examples/*.ts`
|
|
55
|
+
(`// #region <name>` … `// #endregion <name>`). README.md quotes one after
|
|
56
|
+
`<!-- example: examples/<file>#<name> -->`; a docs page in `site/docs-src/` names one with
|
|
57
|
+
`<!-- code examples/<file>#<name> -->`, and the site build puts the code in. Edit the example, not
|
|
58
|
+
a copy: `docs-examples.test.ts` fails when a README copy differs, a marker names no region, or a
|
|
59
|
+
region is shown nowhere, and `examples.integration.test.ts` runs the examples against PostgreSQL.
|
|
60
|
+
|
|
61
|
+
The website and docs live in `site/`. Document a topic on its page in `site/docs-src/`, the one
|
|
62
|
+
place it is written down. The README is the front door: it says what Slotlock is, how to run it,
|
|
63
|
+
and links to those pages; `readme.test.ts` keeps it short and its links working. Its intro, "Why
|
|
64
|
+
it is built for agents", "Project status and boundaries" and "Supported versions" also appear on
|
|
65
|
+
the site, which includes them.
|
|
59
66
|
|
|
60
67
|
## Pull requests
|
|
61
68
|
|