slotlock 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +455 -0
  2. package/CONTRIBUTING.md +55 -19
  3. package/GOVERNANCE.md +4 -2
  4. package/MAINTAINERS.md +3 -2
  5. package/NOTICE +4 -4
  6. package/README.md +403 -103
  7. package/SECURITY.md +70 -5
  8. package/SPEC.md +161 -21
  9. package/agent-server.d.ts +76 -10
  10. package/agent-server.js +381 -146
  11. package/agent-store-backend.d.ts +22 -0
  12. package/agent-store-backend.js +598 -24
  13. package/api-keys.d.ts +22 -7
  14. package/api-keys.js +38 -8
  15. package/application-api.d.ts +67 -0
  16. package/application-api.js +552 -0
  17. package/config.d.ts +93 -0
  18. package/config.js +192 -0
  19. package/dashboard.d.ts +10 -1
  20. package/dashboard.js +23 -5
  21. package/ddl.d.ts +47 -4
  22. package/ddl.js +679 -25
  23. package/engine.js +20 -6
  24. package/event-schema.d.ts +59 -0
  25. package/event-schema.js +85 -0
  26. package/hooks.d.ts +7 -0
  27. package/hooks.js +13 -0
  28. package/ical-worker-thread.js +4 -2
  29. package/ical-worker.d.ts +8 -5
  30. package/ical-worker.js +9 -2
  31. package/index.d.ts +12 -8
  32. package/index.js +7 -5
  33. package/maintenance.d.ts +67 -0
  34. package/maintenance.js +213 -0
  35. package/node-server.d.ts +9 -1
  36. package/node-server.js +127 -6
  37. package/openapi.json +6024 -0
  38. package/package.json +3 -2
  39. package/rate-limit.d.ts +71 -0
  40. package/rate-limit.js +718 -0
  41. package/rest-api.d.ts +33 -18
  42. package/rest-api.js +141 -63
  43. package/rules.d.ts +19 -0
  44. package/rules.js +38 -7
  45. package/self-host.d.ts +26 -9
  46. package/self-host.js +427 -47
  47. package/slotlock-manifest.schema.json +6 -1
  48. package/slotlock.manifest.json +7 -2
  49. package/store.d.ts +359 -27
  50. package/store.js +2077 -296
  51. package/sync.d.ts +43 -2
  52. package/sync.js +1950 -277
  53. package/timezone.d.ts +77 -3
  54. package/timezone.js +265 -21
  55. package/types.d.ts +217 -10
  56. package/weekly-hours.js +2 -2
@@ -66,7 +66,12 @@
66
66
  "mcp-human-confirmation",
67
67
  "mcp-calendar-resources",
68
68
  "mcp-live-updates",
69
- "trace-context-propagation"
69
+ "trace-context-propagation",
70
+ "rest-api",
71
+ "application-api",
72
+ "api-keys",
73
+ "dashboard",
74
+ "maintenance"
70
75
  ]
71
76
  }
72
77
  },
@@ -3,7 +3,7 @@
3
3
  "schemaVersion": "1.0",
4
4
  "name": "Slotlock",
5
5
  "package": "slotlock",
6
- "version": "0.1.1",
6
+ "version": "0.3.0",
7
7
  "description": "Resource-first, deterministic calendar and availability engine for AI agents.",
8
8
  "intervalSemantics": {
9
9
  "boundary": "[)",
@@ -36,7 +36,12 @@
36
36
  "mcp-human-confirmation",
37
37
  "mcp-calendar-resources",
38
38
  "mcp-live-updates",
39
- "trace-context-propagation"
39
+ "trace-context-propagation",
40
+ "rest-api",
41
+ "application-api",
42
+ "api-keys",
43
+ "dashboard",
44
+ "maintenance"
40
45
  ],
41
46
  "transportPolicy": "adapter-defined",
42
47
  "externalCalendarsAreAuthority": false
package/store.d.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import type { Sql, TransactionSql } from 'postgres';
2
- import type { AcquireHoldResult, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventRetentionResult, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CancelCalendarEventResult, CancelExternalReservationResult, ConfirmHoldResult, CreateReservationResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockCalendarEvent, SlotlockResource, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, WeeklyAvailabilityRule } from './types.js';
3
- export declare const SLOTLOCK_CALENDAR_HORIZON_DAYS = 367;
2
+ import { SLOTLOCK_CALENDAR_HORIZON_DAYS } from './rules.js';
3
+ import type { AcquireHoldResult, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventRetentionResult, BookingRetentionResult, SlotlockTenantErasure, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CancelCalendarEventResult, CancelExternalReservationResult, ConfirmHoldResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockCalendarEvent, SlotlockHold, SlotlockReservation, SlotlockResource, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, WeeklyAvailabilityRule } from './types.js';
4
+ export { SLOTLOCK_CALENDAR_HORIZON_DAYS };
4
5
  /**
5
6
  * Longest one-off event the store accepts (ten years and some leap days): long enough for any lease
6
7
  * or long-term rental, short enough that a unit error cannot occupy a resource for ever. A recurring
7
8
  * series is bounded by its 367-day materialization window instead.
8
9
  */
9
10
  export declare const SLOTLOCK_MAX_EVENT_DURATION_DAYS = 3660;
11
+ /**
12
+ * Longest window a recurring series is stored (materialized) for: the 367-day search window plus
13
+ * two days. The rolling horizon starts a day before the current UTC day and must reach 367 days
14
+ * past its end, so that a search of the full 367 days from any moment of the day is complete.
15
+ */
16
+ export declare const SLOTLOCK_MATERIALIZATION_WINDOW_DAYS: number;
10
17
  /** Trusted provider/maintenance namespace used by backwards-compatible store callers. */
11
18
  export declare const SLOTLOCK_INTERNAL_EVENT_OWNER_REF = "internal";
12
19
  /** Default hard ceiling for retained active + tombstoned identities per agent principal. */
@@ -20,11 +27,39 @@ export declare const SLOTLOCK_AGENT_OWNER_COMMAND_QUOTA = 10000;
20
27
  export declare const SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS = 30;
21
28
  /** Store-owned coverage source for bounded, locally materialized recurring events. */
22
29
  export declare const SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE = "slotlock:local-recurrence-materialization";
23
- /** Stable daily target shared by the event writer, maintenance worker, and readiness proof. */
30
+ /**
31
+ * Stable daily target shared by the event writer, maintenance worker, and readiness proof: from a
32
+ * day before the current UTC day to 367 days past its end (SLOTLOCK_MATERIALIZATION_WINDOW_DAYS).
33
+ */
24
34
  export declare function calendarEventRollingHorizon(now?: Date): Interval;
25
35
  /** What `createSlotlockStore` accepts: a `postgres` client, or a transaction/savepoint from one. */
26
36
  export type SlotlockSql = Sql | TransactionSql;
27
37
  type StoreSql = SlotlockSql;
38
+ /** How a migration (applySchema, applyTenantRls, grantApplicationRole) waits and how long it runs. */
39
+ export interface SlotlockMigrationOptions {
40
+ /**
41
+ * The longest it waits for any one lock, in milliseconds (1-600,000; default 1,000). While a
42
+ * migration waits for a lock on a table, every query that comes after it on that table waits
43
+ * behind it; past this it gives up, lets them through, and tries again.
44
+ */
45
+ lockTimeoutMs?: number;
46
+ /** The longest any one of its statements runs, in milliseconds (1-86,400,000; default 900,000). */
47
+ statementTimeoutMs?: number;
48
+ /** How many times it tries in all (1-20; default 5) before refusing with `schema_lock_timeout`. */
49
+ attempts?: number;
50
+ }
51
+ /**
52
+ * The schema version recorded by the last migration, or `null` when there is none this role can
53
+ * read: no schema, a schema from 0.2 or earlier, or a role not yet granted the function (run
54
+ * `slotlock migrate`, which grants it).
55
+ */
56
+ export declare function readSlotlockSchemaVersion(sql: SlotlockSql): Promise<number | null>;
57
+ /**
58
+ * Refuse to serve a schema another release migrated: `schema_outdated` when it is older than this
59
+ * release (or unversioned), so `slotlock migrate` must run first; `schema_newer` when a newer release
60
+ * migrated it, whose schema this release's code does not know. Both carry `installed` and `expected`.
61
+ */
62
+ export declare function assertSlotlockSchemaCurrent(sql: SlotlockSql): Promise<void>;
28
63
  export interface SlotlockStore {
29
64
  /**
30
65
  * Apply the Slotlock DDL (idempotent). Owns the `slotlock` schema on the connected DB. Refuses
@@ -32,16 +67,24 @@ export interface SlotlockStore {
32
67
  * connected one (superusers aside) owns or can create objects in, that holds an object such a
33
68
  * role owns (an extension included), or whose tables such a role may add triggers to or have a
34
69
  * trigger running a function such a role owns. Ownership through a group role counts as another
35
- * role's: deploy as the owning role itself (`SET ROLE`).
70
+ * role's: deploy as the owning role itself (`SET ROLE`). Refuses (`schema_newer`) a schema a newer
71
+ * release migrated, changing nothing. Records SLOTLOCK_SCHEMA_VERSION. A schema this release
72
+ * already migrated, unchanged since, is left as it is without a lock on any table. Constraints an
73
+ * upgrade adds NOT VALID are validated in a second transaction, whose locks no serving read or
74
+ * write waits for (one inside a caller's transaction shares its locks until it ends); one that old
75
+ * rows still break stays NOT VALID, and each later migration validates it again. Each lock it
76
+ * waits for is bounded by `lockTimeoutMs`, after which it tries again (`schema_lock_timeout` once
77
+ * out of attempts).
36
78
  */
37
- applySchema(): Promise<void>;
79
+ applySchema(options?: SlotlockMigrationOptions): Promise<void>;
38
80
  /**
39
81
  * Atomically enable forced tenant RLS using this store's configured context setting. Refuses the
40
- * same `slotlock` schemas as `applySchema()`.
82
+ * same `slotlock` schemas as `applySchema()`. Tables already forced under exactly the policies it
83
+ * installs, bound to this setting, are left as they are without a lock on any table.
41
84
  */
42
85
  applyTenantRls(options?: {
43
86
  allowRebind?: boolean;
44
- }): Promise<void>;
87
+ } & SlotlockMigrationOptions): Promise<void>;
45
88
  /**
46
89
  * Grant an existing application role exactly what the store needs at runtime: USAGE on the
47
90
  * `slotlock` schema and DML on its tables. Run as the deployment role after every `applySchema()`.
@@ -54,23 +97,30 @@ export interface SlotlockStore {
54
97
  * setting. The check runs when the grant is made, so a right added later is not
55
98
  * detected until it runs again, and it is not exhaustive (SECURITY.md lists what it does not read).
56
99
  */
57
- grantApplicationRole(role: string): Promise<void>;
100
+ grantApplicationRole(role: string, options?: SlotlockMigrationOptions): Promise<void>;
58
101
  /**
59
- * Run store operations with one transaction-local tenant context on the same connection. With
60
- * `isolation: 'read committed'` the transaction is READ COMMITTED whatever the role's default (a
61
- * capped `createResource` needs it); inside a caller's transaction of another level it refuses
62
- * (`invalid_transaction_isolation`).
102
+ * Run store operations with one transaction-local tenant context on the same connection. On a
103
+ * client it opens a READ COMMITTED transaction, whatever the role's default. Inside a caller's
104
+ * transaction of another level its writes are refused (`invalid_transaction_isolation`), since
105
+ * that snapshot can miss a suspension, and its reads go on; with `isolation: 'read committed'`
106
+ * such a transaction is refused outright. The serving role needs EXECUTE on
107
+ * slotlock.tenant_suspended(text): without it this refuses with `application_role_not_granted`
108
+ * (grantApplicationRole grants it), and on a schema without it with `schema_outdated`. With the
109
+ * store's `maxTenantTransactions`, an aborted `signal` takes a call still waiting for its turn out
110
+ * of the line, refused with the signal's reason.
63
111
  */
64
112
  withTenant<T>(tenantRef: string, callback: (store: SlotlockTenantStore) => Promise<T>, options?: {
65
113
  isolation?: 'read committed';
114
+ signal?: AbortSignal;
66
115
  }): Promise<T>;
67
116
  /**
68
117
  * INVARIANT (erasure completeness): a tenant integration must use a stable, erasable business
69
118
  * identifier for externalRef and include this schema in its own data-erasure workflow.
70
119
  *
71
- * With `maxTenantResources` (1-100,000, tenant-owned resources only) a new resource is refused
72
- * (`resource_limit_reached`) once the tenant has that many; an existing `externalRef` is still
73
- * updated. Capped creates for one tenant run one at a time under a database lock, so racing ones
120
+ * With `maxTenantResources` (1-100,000, tenant-owned resources only), or the store's `resources`
121
+ * limit (the lower of the two when both are set), a new resource of a tenant is refused
122
+ * (`resource_limit_reached`, with the `limit`) once the tenant has that many; an existing
123
+ * `externalRef` is still updated. Capped creates for one tenant run one at a time under a database lock, so racing ones
74
124
  * on any number of servers stop at the cap. The count needs READ COMMITTED: a capped create opens
75
125
  * one itself, and inside a caller's transaction of another level it refuses
76
126
  * (`invalid_transaction_isolation`). Uncapped creates do not wait for the lock and are not
@@ -81,12 +131,46 @@ export interface SlotlockStore {
81
131
  tenantRef?: string;
82
132
  timezone?: string;
83
133
  maxTenantResources?: number;
134
+ /**
135
+ * `shared` (the default) or `private`, which agents never see. Fixed at creation: creating an
136
+ * existing reference again with none named returns it as it is, and with another visibility is
137
+ * refused (`resource_visibility_conflict`).
138
+ */
139
+ visibility?: SlotlockResource['visibility'];
84
140
  }): Promise<SlotlockResource>;
85
- /** Resolve one tenant-owned resource without revealing another tenant's matching id. */
141
+ /**
142
+ * Resolve one tenant-owned resource without revealing another tenant's matching id. An archived
143
+ * one is returned (with `archivedAt`); with `visibility`, only one of that visibility.
144
+ */
86
145
  getResource(params: {
87
146
  tenantRef: string;
88
147
  id: string;
148
+ visibility?: SlotlockResource['visibility'];
89
149
  }): Promise<SlotlockResource | null>;
150
+ /**
151
+ * Archive one of the tenant's resources: no list shows it and it takes no new booking, hold or
152
+ * event (`resource_archived`), while what is on it stays and can still be cancelled or released.
153
+ * Archiving it again keeps the first time. Null for an id that is not the tenant's.
154
+ */
155
+ archiveResource(params: {
156
+ tenantRef: string;
157
+ id: string;
158
+ }): Promise<SlotlockResource | null>;
159
+ /** Put an archived resource back in use. Null for an id that is not the tenant's. */
160
+ restoreResource(params: {
161
+ tenantRef: string;
162
+ id: string;
163
+ }): Promise<SlotlockResource | null>;
164
+ /**
165
+ * Delete one of the tenant's resources with its history (its past bookings, coverage and the
166
+ * occupation of cancelled events), freeing its place under the resource cap. Refused
167
+ * (`resource_in_use`) while a booking on it has not ended, a hold on it is live, or a calendar
168
+ * event (one-off not yet over, or recurring) is on it: cancel or move those first.
169
+ */
170
+ deleteResource(params: {
171
+ tenantRef: string;
172
+ id: string;
173
+ }): Promise<DeleteResourceResult>;
90
174
  /**
91
175
  * Set one resource's own weekly bookable hours, or clear them (`null`) so it uses the default;
92
176
  * `[]` makes it never bookable. Null for an id that is not the tenant's. Refuses rules Slotlock
@@ -107,20 +191,32 @@ export interface SlotlockStore {
107
191
  tenantRef: string;
108
192
  limit?: number;
109
193
  after?: string;
194
+ /** Only resources of this visibility; omitted, both. */
195
+ visibility?: SlotlockResource['visibility'];
196
+ /** Archived resources too; omitted, only those in use. */
197
+ includeArchived?: boolean;
110
198
  }): Promise<SlotlockResource[]>;
111
199
  /**
112
200
  * Insert a confirmed reservation with the EXCLUDE constraint as the overlap arbiter. Never
113
201
  * throws for the expected outcomes: an invalid window or a lost race come back structured.
114
202
  * Expired holds overlapping the window are lazily reaped in the same transaction — an
115
- * expired-but-unpurged hold can never block (no cron required).
203
+ * expired-but-unpurged hold can never block (no cron required). On a tenant's resource the
204
+ * booking takes the tenant, and its own id as reference, so cancel and reschedule reach it. With
205
+ * `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
206
+ * connection's row-level security).
116
207
  */
117
208
  createReservation(params: {
118
209
  resourceId: string;
119
210
  start: Date;
120
211
  end: Date;
121
212
  source?: string;
213
+ tenantRef?: string;
122
214
  }): Promise<CreateReservationResult>;
123
- /** Idempotently create a tenant-owned booking reservation by its stable business identity. */
215
+ /**
216
+ * Idempotently create a tenant-owned booking reservation by its stable business identity. The
217
+ * rows a calendar event occupies are not bookings: here, in reschedule and in cancel their
218
+ * references answer as references no booking holds.
219
+ */
124
220
  createExternalReservation(params: ExternalReservationCommand): Promise<ExternalReservationResult>;
125
221
  /** Atomically move an existing tenant-owned booking; a conflict leaves its old range intact. */
126
222
  rescheduleExternalReservation(params: ExternalReservationCommand): Promise<ExternalReservationResult>;
@@ -133,6 +229,8 @@ export interface SlotlockStore {
133
229
  /**
134
230
  * Occupy a window WITHOUT committing: the hold blocks rivals under the same EXCLUDE arbiter
135
231
  * until `expiresAt` (DB clock — now() + ttl, never the app clock), then frees via lazy reap.
232
+ * With `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
233
+ * connection's row-level security).
136
234
  */
137
235
  acquireHold(params: {
138
236
  resourceId: string;
@@ -140,13 +238,81 @@ export interface SlotlockStore {
140
238
  end: Date;
141
239
  ttlMs: number;
142
240
  source?: string;
241
+ tenantRef?: string;
242
+ /** Turnaround kept busy after `end`, 0 to 30 days; the confirmed booking keeps it. */
243
+ bufferAfterMs?: number;
244
+ /**
245
+ * A retry identity, 1-500 UTF-8 bytes, unique inside `tenantRef` and `principalRef` (both
246
+ * required with it): a request repeated with the same key and body answers the hold the first
247
+ * made (`idempotent: true`), and one with another body is `idempotency_conflict`. The answer is
248
+ * kept until the hold would have expired, released or confirmed; a retry after that makes a new
249
+ * hold.
250
+ */
251
+ idempotencyKey?: string;
252
+ /** Whose keys `idempotencyKey` is among, 1-500 UTF-8 bytes (an API key's subject, say). */
253
+ principalRef?: string;
143
254
  }): Promise<AcquireHoldResult>;
144
- /** Flip an unexpired hold to a confirmed reservation (expiry cleared). */
145
- confirmHold(holdId: string): Promise<ConfirmHoldResult>;
146
- /** Delete a live hold; released=false when it was already gone (expired-reaped or confirmed). */
147
- releaseHold(holdId: string): Promise<ReleaseHoldResult>;
148
- /** Reservations overlapping `window` for one resource, as merged-ready half-open intervals. */
149
- listBusy(resourceId: string, window: Interval): Promise<Interval[]>;
255
+ /** One of the tenant's live holds, or null (none, expired, confirmed, or another tenant's). */
256
+ getHold(params: {
257
+ tenantRef: string;
258
+ holdId: string;
259
+ }): Promise<SlotlockHold | null>;
260
+ /** The tenant's live holds on one resource, by start then id, keyset-paginated like events. */
261
+ listHolds(params: {
262
+ tenantRef: string;
263
+ resourceId: string;
264
+ limit?: number;
265
+ after?: {
266
+ start: Date;
267
+ id: string;
268
+ };
269
+ }): Promise<SlotlockHold[]>;
270
+ /**
271
+ * The tenant's booking with this reference as it now is (its current revision), or null: none,
272
+ * cancelled, or a calendar event's occupation, which is not a booking.
273
+ */
274
+ getReservation(params: {
275
+ tenantRef: string;
276
+ externalRef: string;
277
+ }): Promise<SlotlockReservation | null>;
278
+ /**
279
+ * The tenant's bookings on one resource whose occupied time meets `window` (at most 367 days),
280
+ * by start then id, keyset-paginated: pass the last one's `{ start, id }` as `after`.
281
+ */
282
+ listReservations(params: {
283
+ tenantRef: string;
284
+ resourceId: string;
285
+ window: Interval;
286
+ limit?: number;
287
+ after?: {
288
+ start: Date;
289
+ id: string;
290
+ };
291
+ }): Promise<SlotlockReservation[]>;
292
+ /**
293
+ * Flip an unexpired hold to a confirmed booking (expiry cleared). On a tenant's resource the
294
+ * booking takes the tenant, and the hold's id as its reference at revision 1, so cancel and
295
+ * reschedule reach it like any other (`reservation_cancelled` when a cancel of that reference
296
+ * came first). Confirming it again answers the booking as it now is. With `tenantRef`, only a
297
+ * hold on one of that tenant's resources counts: another tenant's is `hold_not_found` whatever
298
+ * the connection's row-level security.
299
+ */
300
+ confirmHold(holdId: string, options?: SlotlockHoldTenantOptions): Promise<ConfirmHoldResult>;
301
+ /**
302
+ * Delete a live hold; released=false when it was already gone (expired-reaped or confirmed). With
303
+ * `tenantRef`, only a hold on one of that tenant's resources, as for confirmHold.
304
+ */
305
+ releaseHold(holdId: string, options?: SlotlockHoldTenantOptions): Promise<ReleaseHoldResult>;
306
+ /**
307
+ * Reservations overlapping `window` for one resource, as merged-ready half-open intervals. With
308
+ * `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
309
+ * connection's row-level security). An id the store never issues is `invalid_identity`. With
310
+ * `limit`, at most that many, the first by start.
311
+ */
312
+ listBusy(resourceId: string, window: Interval, options?: {
313
+ tenantRef?: string;
314
+ limit?: number;
315
+ }): Promise<Interval[]>;
150
316
  /** Create or revise one trusted event under expected-revision + tenant idempotency guards. */
151
317
  putCalendarEvent(params: PutCalendarEventCommand): Promise<PutCalendarEventResult>;
152
318
  /** Read one active event. A cancelled/tombstoned identity reads as absent. */
@@ -156,11 +322,24 @@ export interface SlotlockStore {
156
322
  ownerRef?: string;
157
323
  externalRef: string;
158
324
  }): Promise<SlotlockCalendarEvent | null>;
325
+ /**
326
+ * One of the tenant's active events by id, whichever owner (the tenant itself or an agent key)
327
+ * wrote it: null when there is none, it was cancelled, or it is another tenant's.
328
+ */
329
+ getCalendarEventById(params: {
330
+ tenantRef: string;
331
+ eventId: string;
332
+ }): Promise<SlotlockCalendarEvent | null>;
159
333
  /** List active events with at least one materialized occurrence intersecting the window. */
160
334
  listCalendarEvents(params: {
161
335
  tenantRef: string;
162
336
  /** Defaults to the trusted `internal` namespace. */
163
337
  ownerRef?: string;
338
+ /**
339
+ * Every owner's events, each with its `ownerRef`, so a tenant reaches what any of its keys
340
+ * booked, a revoked one's included. Not with `ownerRef`.
341
+ */
342
+ allOwners?: boolean;
164
343
  resourceId: string;
165
344
  window: Interval;
166
345
  limit?: number;
@@ -178,10 +357,34 @@ export interface SlotlockStore {
178
357
  externalRef: string;
179
358
  idempotencyKey: string;
180
359
  expectedRevision: number;
360
+ /**
361
+ * The resource the writer read the event on: `event_not_found` when the stored event is on
362
+ * another, checked under the event's lock.
363
+ */
364
+ expectedResourceId?: string;
365
+ }): Promise<CancelCalendarEventResult>;
366
+ /**
367
+ * The tenant's cancellation of one of its events by id, whichever owner wrote it: an agent key's
368
+ * included, after the key is revoked and when it has spent its command quota. The event's
369
+ * identity is cancelled (its tombstone is its owner's, so that key cannot re-create it), and the
370
+ * command is the tenant's own: its idempotency key is in the trusted `internal` namespace, where
371
+ * it replays. A cancelled event, an unknown id or another tenant's is `event_not_found`.
372
+ */
373
+ cancelCalendarEventById(params: {
374
+ tenantRef: string;
375
+ eventId: string;
376
+ idempotencyKey: string;
377
+ expectedRevision: number;
378
+ /** As for cancelCalendarEvent: `event_not_found` when the event is on another resource. */
379
+ expectedResourceId?: string;
181
380
  }): Promise<CancelCalendarEventResult>;
182
381
  /**
183
382
  * Atomically rematerialize due recurring masters into one rolling window without changing their
184
383
  * semantic revision. A collision rolls back that master and leaves its old coverage untouched.
384
+ * A master is rolled from the window's start through the later of its stored end and the
385
+ * window's, so its end never moves back. One that already covers the window, or whose end lies
386
+ * too far past it for the 367-day cap from the window's start, is not due: it is left as it is
387
+ * and not counted.
185
388
  */
186
389
  rollCalendarEventHorizon(params: {
187
390
  tenantRef: string;
@@ -205,6 +408,59 @@ export interface SlotlockStore {
205
408
  /** Replay window in whole days, 1-3650. Defaults to `SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS`. */
206
409
  retentionDays?: number;
207
410
  }): Promise<CalendarEventRetentionResult>;
411
+ /**
412
+ * Delete one bounded batch of a tenant's dead booking rows: holds past their expiry and the
413
+ * answers kept for keyed ones (at once), and references retired by a cancel no booking held,
414
+ * older than the retention window. A cancelled booking's tombstone is never deleted here (only by
415
+ * erasing the tenant), so its reference is never booked again. Rows another transaction holds
416
+ * are skipped, not waited on. It also gives a confirmed booking a 0.2 server wrote without its
417
+ * tenant the tenant and its own id as reference, as the migration does. Call until `hasMore` is
418
+ * false.
419
+ */
420
+ pruneBookingRetention(params: {
421
+ tenantRef: string;
422
+ /** Rows per class per call, 1-1000. Defaults to 100. */
423
+ limit?: number;
424
+ /**
425
+ * How long a stray retirement is kept, in whole days, 1-3650. Defaults to
426
+ * `SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS`.
427
+ */
428
+ retentionDays?: number;
429
+ }): Promise<BookingRetentionResult>;
430
+ /**
431
+ * Suspend a tenant: from when this returns, every withTenant transaction for it (as every server
432
+ * runs its requests) may read, and its writes are refused (`tenant_suspended`); one in flight is
433
+ * waited for. Its keys still authenticate, but it gets no new key or secret. Maintenance and
434
+ * erasure go on. True when it was not suspended already. Only the deployment role, which owns the
435
+ * schema, may suspend (`tenant_suspension_not_permitted` for another).
436
+ */
437
+ suspendTenant(params: {
438
+ tenantRef: string;
439
+ /** Why, for the operator: 1-500 characters without control characters. */
440
+ reason?: string;
441
+ }): Promise<{
442
+ suspended: boolean;
443
+ }>;
444
+ /** Lift a suspension; true when the tenant was suspended. The deployment role only. */
445
+ unsuspendTenant(params: {
446
+ tenantRef: string;
447
+ }): Promise<{
448
+ unsuspended: boolean;
449
+ }>;
450
+ /** Whether the tenant is suspended. */
451
+ isTenantSuspended(params: {
452
+ tenantRef: string;
453
+ }): Promise<boolean>;
454
+ /**
455
+ * Erase a tenant: delete every row it owns (resources, holds, bookings, events with their
456
+ * attendees and reminders, cancellations, idempotency records, coverage, API keys, and a personal
457
+ * tenant's dashboard records) in one transaction, waiting for its transactions in flight. API key
458
+ * digests are retired so no erased key works again; a suspension stays. Erasing again deletes
459
+ * nothing.
460
+ */
461
+ eraseTenant(params: {
462
+ tenantRef: string;
463
+ }): Promise<SlotlockTenantErasure>;
208
464
  /** Record the bounded, complete source window represented by a provider cursor/revision. */
209
465
  recordCalendarCoverage(params: CalendarCoverageCommand): Promise<CalendarCoverageResult>;
210
466
  /** Return merged busy intervals plus explicit completeness for caller-required provider sources. */
@@ -215,12 +471,27 @@ export interface SlotlockStore {
215
471
  requiredSources?: string[];
216
472
  coverageMaxAgeMs?: number;
217
473
  }): Promise<CalendarFreeBusy>;
218
- /** Convenience composition: listBusy × expandRules (in the resource's stored timezone) → findNextAvailable. */
474
+ /**
475
+ * The first slot of `durationMs` inside the hours, whose time and turnaround buffer are free:
476
+ * getFreeBusy × expandRules (in the resource's stored timezone) → findNextAvailable. Null when no
477
+ * slot is free, and when one cannot be proven free: the coverage of the window (with the buffer's
478
+ * tail) is not complete, because a recurring series is not stored that far or a required provider
479
+ * calendar is not read that far. Another tenant's resource, an unknown id or one the store never
480
+ * issues is `resource_not_found`. A search window longer than 367 days is `invalid_window`.
481
+ */
219
482
  findNextAvailableFor(params: {
483
+ /** The resource's tenant; `null` for a resource that has none (the legacy namespace). */
484
+ tenantRef: string | null;
220
485
  resourceId: string;
221
- rules: WeeklyAvailabilityRule[];
486
+ /** The hours to search; omitted, the resource's own (none: no slot). */
487
+ rules?: WeeklyAvailabilityRule[];
222
488
  searchWindow: Interval;
223
489
  durationMs: number;
490
+ /** Turnaround after the slot that must be free too (it may run past the hours), 0-30 days. */
491
+ bufferAfterMs?: number;
492
+ /** Provider sources that must prove complete coverage, as for getFreeBusy. */
493
+ requiredSources?: string[];
494
+ coverageMaxAgeMs?: number;
224
495
  }): Promise<Interval | null>;
225
496
  }
226
497
  export type SlotlockTenantStore = Omit<SlotlockStore, 'applySchema' | 'applyTenantRls' | 'grantApplicationRole' | 'withTenant'>;
@@ -231,6 +502,67 @@ export interface SlotlockStoreOptions {
231
502
  agentOwnerEventQuota?: number;
232
503
  /** Hard retained-command ceiling per non-internal owner. Defaults to 10,000; cannot be disabled. */
233
504
  agentOwnerCommandQuota?: number;
505
+ /**
506
+ * What one tenant may store, for a database tenants share (none by default). A write that would
507
+ * pass a limit is refused with it (`*_limit_reached`); what the tenant has can still be revised
508
+ * (a move adds no booking or event, so it is not refused), cancelled and released. Counted under
509
+ * a lock per tenant and limit, so racing writers never pass it, in READ COMMITTED transactions
510
+ * only (`invalid_transaction_isolation` otherwise). Bookings and events count while they are still
511
+ * to come, and for 30 days after they were made, whatever time they hold; history older than that
512
+ * does not count, and a count stops reading at the limit.
513
+ */
514
+ tenantLimits?: SlotlockTenantLimits;
515
+ /**
516
+ * How many withTenant transactions of one tenant run at once on this store (1-1,000; no limit by
517
+ * default). The rest wait in this process, in order, so one tenant's transactions waiting on its
518
+ * own locks hold at most this many of a pool's connections. Not counted inside a caller's
519
+ * transaction, which holds its connection already.
520
+ */
521
+ maxTenantTransactions?: number;
522
+ /**
523
+ * The longest a withTenant transaction waits for any one lock, in milliseconds (1-600,000; none by
524
+ * default). Past it the transaction is rolled back and refused with `lock_timeout`, which is safe
525
+ * to retry. Not set inside a caller's transaction.
526
+ */
527
+ tenantLockTimeoutMs?: number;
234
528
  }
529
+ /** Per-tenant row limits, each a whole number from 1 to 10,000,000 (resources: to 100,000). */
530
+ export interface SlotlockTenantLimits {
531
+ /** Resources, archived ones included; the lower of this and a call's `maxTenantResources`. */
532
+ resources?: number;
533
+ /**
534
+ * Holds on the tenant's resources, expired ones included until they are deleted. A keyed hold
535
+ * keeps its place until it would have expired, released or confirmed, while its answer is kept
536
+ * for retries. A hold at the limit first deletes the tenant's expired holds and forgets their
537
+ * answers, as maintenance does, so only live holds keep a new one out.
538
+ */
539
+ holds?: number;
540
+ /**
541
+ * Bookings still to come or made in the last 30 days, cancellations made in the last 30 days,
542
+ * and references a cancel retired with no booking until maintenance prunes them.
543
+ */
544
+ reservations?: number;
545
+ /**
546
+ * Calendar events of every owner still to come (a series until it is known to have ended) or made
547
+ * in the last 30 days, cancellations of every owner made in the last 30 days, and agent-owned
548
+ * cancellations until maintenance prunes them. Each event stores at most 2,000 occurrences.
549
+ */
550
+ events?: number;
551
+ /** Provider coverage rows: one per resource and source. */
552
+ coverage?: number;
553
+ }
554
+ export interface SlotlockHoldTenantOptions {
555
+ /** Only a hold on one of this tenant's resources: 1-500 UTF-8 bytes. */
556
+ tenantRef?: string;
557
+ }
558
+ /**
559
+ * At most `max` holders per tenant at once; the rest wait in order. An aborted signal takes a
560
+ * waiter out of the line, rejecting with its reason. Each holder calls the release it gets once.
561
+ */
562
+ export declare function createTenantSlots(max: number): (tenantRef: string, signal?: AbortSignal) => Promise<() => void>;
235
563
  export declare function createSlotlockStore(sql: StoreSql, options?: SlotlockStoreOptions): SlotlockStore;
236
- export {};
564
+ /**
565
+ * Every store method that writes for a tenant: what a suspended tenant's withTenant store refuses.
566
+ * Maintenance (rolling series, pruning, purging) and erasure are not among them.
567
+ */
568
+ export declare const SLOTLOCK_TENANT_WRITE_METHODS: readonly ["createResource", "archiveResource", "restoreResource", "deleteResource", "setResourceAvailability", "setTenantAvailability", "createReservation", "createExternalReservation", "rescheduleExternalReservation", "cancelExternalReservation", "acquireHold", "confirmHold", "releaseHold", "putCalendarEvent", "cancelCalendarEvent", "cancelCalendarEventById", "recordCalendarCoverage"];