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.
Files changed (100) hide show
  1. package/CHANGELOG.md +492 -0
  2. package/CONTRIBUTING.md +12 -5
  3. package/README.md +50 -1544
  4. package/SECURITY.md +5 -2
  5. package/SPEC.md +1 -1
  6. package/agent-server.d.ts +160 -8
  7. package/agent-server.js +560 -49
  8. package/agent-store-backend.d.ts +39 -7
  9. package/agent-store-backend.js +1439 -104
  10. package/api-keys.d.ts +69 -3
  11. package/api-keys.js +218 -29
  12. package/application-api.d.ts +59 -6
  13. package/application-api.js +842 -39
  14. package/approvals.d.ts +88 -0
  15. package/approvals.js +293 -0
  16. package/assistant.d.ts +58 -27
  17. package/assistant.js +151 -53
  18. package/billing.d.ts +114 -0
  19. package/billing.js +258 -0
  20. package/booking-rules.d.ts +30 -41
  21. package/booking-rules.js +17 -25
  22. package/change-watch.d.ts +1 -0
  23. package/change-watch.js +1 -1
  24. package/cli.js +3 -4
  25. package/config.d.ts +42 -6
  26. package/config.js +114 -7
  27. package/dashboard-icon.d.ts +1 -0
  28. package/dashboard-icon.js +5 -0
  29. package/dashboard.d.ts +203 -7
  30. package/dashboard.js +1558 -64
  31. package/ddl.d.ts +67 -35
  32. package/ddl.js +2732 -162
  33. package/engine.d.ts +6 -9
  34. package/engine.js +5 -9
  35. package/event-feed.d.ts +53 -35
  36. package/event-feed.js +201 -64
  37. package/event-schema.js +6 -9
  38. package/hooks.d.ts +2 -4
  39. package/hooks.js +2 -4
  40. package/ical-worker-thread.js +3 -4
  41. package/ical-worker.d.ts +8 -10
  42. package/ical-worker.js +7 -9
  43. package/index.d.ts +15 -4
  44. package/index.js +8 -2
  45. package/interop.js +1 -4
  46. package/maintenance.d.ts +24 -10
  47. package/maintenance.js +59 -28
  48. package/mcp-app.d.ts +7 -9
  49. package/mcp-app.js +7 -9
  50. package/mcp-modern.d.ts +14 -16
  51. package/mcp-modern.js +23 -27
  52. package/metrics.d.ts +41 -0
  53. package/metrics.js +22 -1
  54. package/node-server.d.ts +9 -9
  55. package/node-server.js +25 -34
  56. package/openapi.json +14675 -5957
  57. package/otel.d.ts +86 -0
  58. package/otel.js +273 -0
  59. package/overage.d.ts +33 -0
  60. package/overage.js +115 -0
  61. package/package-version.js +2 -4
  62. package/package.json +1 -1
  63. package/partner-billing.d.ts +81 -0
  64. package/partner-billing.js +333 -0
  65. package/partner-names.d.ts +8 -0
  66. package/partner-names.js +10 -0
  67. package/partners.d.ts +147 -0
  68. package/partners.js +375 -0
  69. package/plans.d.ts +92 -0
  70. package/plans.js +115 -0
  71. package/quotas.d.ts +108 -0
  72. package/quotas.js +258 -0
  73. package/rate-limit.d.ts +27 -2
  74. package/rate-limit.js +106 -23
  75. package/resource-profile.d.ts +135 -0
  76. package/resource-profile.js +263 -0
  77. package/rest-api.d.ts +15 -14
  78. package/rest-api.js +79 -15
  79. package/rules.d.ts +6 -7
  80. package/rules.js +30 -42
  81. package/sandbox.d.ts +70 -0
  82. package/sandbox.js +295 -0
  83. package/self-host.d.ts +46 -4
  84. package/self-host.js +990 -41
  85. package/slotlock.manifest.json +1 -1
  86. package/store.d.ts +482 -165
  87. package/store.js +1460 -484
  88. package/sync.d.ts +44 -59
  89. package/sync.js +325 -469
  90. package/teams.d.ts +25 -0
  91. package/teams.js +63 -0
  92. package/timezone.d.ts +25 -33
  93. package/timezone.js +50 -53
  94. package/types.d.ts +130 -57
  95. package/usage.d.ts +106 -0
  96. package/usage.js +238 -0
  97. package/webhooks.d.ts +22 -25
  98. package/webhooks.js +35 -40
  99. package/weekly-hours.d.ts +3 -0
  100. package/weekly-hours.js +1 -1
package/store.d.ts CHANGED
@@ -1,19 +1,20 @@
1
+ import { type SlotlockResourceKind, type SlotlockResourceProfileInput, type SlotlockLocation, type SlotlockPoolOrder, type SlotlockResourceGroup, type SlotlockResourceScope } from './resource-profile.js';
2
+ import { type SlotlockUsageRow } from './usage.js';
1
3
  import { type SlotlockChangeOrigin, type SlotlockEventFeed } from './event-feed.js';
2
4
  import { type SlotlockBookingRules } from './booking-rules.js';
3
5
  import type { Sql, TransactionSql } from 'postgres';
4
6
  import { SLOTLOCK_CALENDAR_HORIZON_DAYS } from './rules.js';
5
- import type { AcquireHoldResult, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventRetentionResult, BookingRetentionResult, SlotlockTenantErasure, SlotlockBookingRuleDefaults, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CancelCalendarEventResult, CancelExternalReservationResult, ConfirmHoldResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockCalendarEvent, SlotlockAgendaEntry, SlotlockHold, SlotlockReservation, SlotlockResource, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, WeeklyAvailabilityRule } from './types.js';
7
+ import type { AcquireHoldResult, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventRetentionResult, BookingRetentionResult, SlotlockTenantErasure, SlotlockBookingRuleDefaults, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CancelCalendarEventResult, CancelExternalReservationResult, ConfirmHoldResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockCalendarEvent, SlotlockAgendaEntry, SlotlockHold, SlotlockBookingDescription, SlotlockApproval, SlotlockApprovalPolicy, SlotlockReservation, SlotlockResource, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, WeeklyAvailabilityRule } from './types.js';
8
+ import { type PutApprovalPolicyParams } from './approvals.js';
6
9
  export { SLOTLOCK_CALENDAR_HORIZON_DAYS };
7
10
  /**
8
- * Longest one-off event the store accepts (ten years and some leap days): long enough for any lease
9
- * or long-term rental, short enough that a unit error cannot occupy a resource for ever. A recurring
10
- * series is bounded by its 367-day materialization window instead.
11
+ * Longest one-off event (ten years and some leap days); a recurring series is bounded by its
12
+ * materialization window instead.
11
13
  */
12
14
  export declare const SLOTLOCK_MAX_EVENT_DURATION_DAYS = 3660;
13
15
  /**
14
- * Longest window a recurring series is stored (materialized) for: the 367-day search window plus
15
- * two days. The rolling horizon starts a day before the current UTC day and must reach 367 days
16
- * past its end, so that a search of the full 367 days from any moment of the day is complete.
16
+ * The 367-day search window plus two days: the rolling horizon starts a day before the current UTC
17
+ * day and must reach 367 days past its end.
17
18
  */
18
19
  export declare const SLOTLOCK_MATERIALIZATION_WINDOW_DAYS: number;
19
20
  /** Trusted provider/maintenance namespace used by backwards-compatible store callers. */
@@ -38,8 +39,8 @@ export declare const SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS = 30;
38
39
  /** Store-owned coverage source for bounded, locally materialized recurring events. */
39
40
  export declare const SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE = "slotlock:local-recurrence-materialization";
40
41
  /**
41
- * Stable daily target shared by the event writer, maintenance worker, and readiness proof: from a
42
- * day before the current UTC day to 367 days past its end (SLOTLOCK_MATERIALIZATION_WINDOW_DAYS).
42
+ * From a day before the current UTC day to SLOTLOCK_MATERIALIZATION_WINDOW_DAYS past its end;
43
+ * shared by the writer, maintenance and readiness.
43
44
  */
44
45
  export declare function calendarEventRollingHorizon(now?: Date): Interval;
45
46
  /** What `createSlotlockStore` accepts: a `postgres` client, or a transaction/savepoint from one. */
@@ -48,9 +49,9 @@ type StoreSql = SlotlockSql;
48
49
  /** How a migration (applySchema, applyTenantRls, grantApplicationRole) waits and how long it runs. */
49
50
  export interface SlotlockMigrationOptions {
50
51
  /**
51
- * The longest it waits for any one lock, in milliseconds (1-600,000; default 1,000). While a
52
- * migration waits for a lock on a table, every query that comes after it on that table waits
53
- * behind it; past this it gives up, lets them through, and tries again.
52
+ * The longest it waits for any one lock, in milliseconds (1-600,000; default 1,000). Queries on a
53
+ * table queue behind a migration waiting for its lock, so past this it gives up, lets them
54
+ * through, and retries.
54
55
  */
55
56
  lockTimeoutMs?: number;
56
57
  /** The longest any one of its statements runs, in milliseconds (1-86,400,000; default 900,000). */
@@ -59,21 +60,18 @@ export interface SlotlockMigrationOptions {
59
60
  attempts?: number;
60
61
  }
61
62
  /**
62
- * The schema version recorded by the last migration, or `null` when there is none this role can
63
- * read: no schema, a schema from 0.2 or earlier, or a role not yet granted the function (run
64
- * `slotlock migrate`, which grants it).
63
+ * The recorded schema version, or `null` when this role cannot read one: no schema, 0.2 or earlier,
64
+ * or the function not yet granted (`slotlock migrate` grants it).
65
65
  */
66
66
  export declare function readSlotlockSchemaVersion(sql: SlotlockSql): Promise<number | null>;
67
67
  /**
68
- * Refuse to serve a schema another release migrated: `schema_outdated` when it is older than this
69
- * release (or unversioned), so `slotlock migrate` must run first; `schema_newer` when a newer release
70
- * migrated it, whose schema this release's code does not know. Both carry `installed` and `expected`.
68
+ * Refuse a schema another release migrated: `schema_outdated` (older or unversioned: run `slotlock
69
+ * migrate`) or `schema_newer`. Both carry `installed` and `expected`.
71
70
  */
72
71
  export declare function assertSlotlockSchemaCurrent(sql: SlotlockSql): Promise<void>;
73
72
  /**
74
- * What makes a put the same request: its identity and the event it asks for. Not the window its
75
- * occurrences are stored over, which for a series is the rolling horizon from the current UTC
76
- * midnight: with it in the hash, a retry after midnight was refused as a different request.
73
+ * A put's identity and the event it asks for, not its storage window: a series' window moves with
74
+ * the UTC day, so a retry after midnight must still match.
77
75
  */
78
76
  declare function putCommandHash(params: {
79
77
  command: PutCalendarEventCommand;
@@ -81,9 +79,8 @@ declare function putCommandHash(params: {
81
79
  event: CalendarEventContent;
82
80
  }): string;
83
81
  /**
84
- * The hash 0.3.0 and earlier stored, with the window in it, so a command stored before the upgrade
85
- * still replays: always for a one-off event, whose window is its own span, and on the same UTC day
86
- * for a series. Remove once no stored command can be that old (commands are pruned after 30 days).
82
+ * The hash 0.3.0 and earlier stored (window included), so an older stored command still replays.
83
+ * Remove once no stored command can be that old (pruned after 30 days).
87
84
  */
88
85
  export declare function legacyPutCommandHash(params: Parameters<typeof putCommandHash>[0] & {
89
86
  window: Interval;
@@ -95,52 +92,38 @@ export declare const SLOTLOCK_MAX_POOL_RULE_DEFAULTS = 1000;
95
92
  export declare function isPoolName(value: unknown): value is string;
96
93
  export interface SlotlockStore extends SlotlockEventFeed {
97
94
  /**
98
- * Apply the Slotlock DDL (idempotent). Owns the `slotlock` schema on the connected DB. Refuses
99
- * (`unsafe_slotlock_schema`, with the `reasons` found) a `slotlock` schema that a role other than the
100
- * connected one (superusers aside) owns or can create objects in, that holds an object such a
101
- * role owns (an extension included), or whose tables such a role may add triggers to or have a
102
- * trigger running a function such a role owns. Ownership through a group role counts as another
103
- * role's: deploy as the owning role itself (`SET ROLE`). Refuses (`schema_newer`) a schema a newer
104
- * release migrated, changing nothing. Records SLOTLOCK_SCHEMA_VERSION. A schema this release
105
- * already migrated, unchanged since, is left as it is without a lock on any table. Constraints an
106
- * upgrade adds NOT VALID are validated in a second transaction, whose locks no serving read or
107
- * write waits for (one inside a caller's transaction shares its locks until it ends); one that old
108
- * rows still break stays NOT VALID, and each later migration validates it again. Each lock it
109
- * waits for is bounded by `lockTimeoutMs`, after which it tries again (`schema_lock_timeout` once
95
+ * Apply the Slotlock DDL (idempotent). Refuses (`unsafe_slotlock_schema`, with `reasons`) a
96
+ * `slotlock` schema that another role (superusers aside) owns, can create objects in, owns an
97
+ * object in, or can add triggers to; ownership through a group role counts, so deploy as the
98
+ * owning role itself (`SET ROLE`). Refuses (`schema_newer`) a schema a newer release migrated. An
99
+ * unchanged schema is left without locking any table. Constraints added NOT VALID are validated
100
+ * in a second transaction; one old rows still break stays NOT VALID and is retried on each
101
+ * migration. Each lock wait is bounded by `lockTimeoutMs` and retried (`schema_lock_timeout` once
110
102
  * out of attempts).
111
103
  */
112
104
  applySchema(options?: SlotlockMigrationOptions): Promise<void>;
113
105
  /**
114
- * Atomically enable forced tenant RLS using this store's configured context setting. Refuses the
115
- * same `slotlock` schemas as `applySchema()`. Tables already forced under exactly the policies it
116
- * installs, bound to this setting, are left as they are without a lock on any table.
106
+ * Atomically enable forced tenant RLS using the store's context setting. Refuses the same schemas
107
+ * as `applySchema()`. Tables already set up exactly so are left without a lock.
117
108
  */
118
109
  applyTenantRls(options?: {
119
110
  allowRebind?: boolean;
120
111
  } & SlotlockMigrationOptions): Promise<void>;
121
112
  /**
122
113
  * Grant an existing application role exactly what the store needs at runtime: USAGE on the
123
- * `slotlock` schema and DML on its tables. Run as the deployment role after every `applySchema()`.
124
- * Refuses (`unsafe_application_role`, with the `reasons` found) a role that has one of the known
125
- * ways around forced tenant RLS or the exclusion arbiter, itself, through a role it belongs to or
126
- * through PUBLIC: superuser, BYPASSRLS, REPLICATION or CREATEROLE, a server file or program role,
127
- * ownership of the database, of anything in it (its temporary objects and its own default
128
- * privileges aside) or of `btree_gist`, CREATE on the database or on any schema, TRUNCATE,
129
- * REFERENCES or TRIGGER on a Slotlock table (or by default on new ones), or a right on a server
130
- * setting. The check runs when the grant is made, so a right added later is not
131
- * detected until it runs again, and it is not exhaustive (SECURITY.md lists what it does not read).
114
+ * `slotlock` schema and DML on its tables. Run as the deployment role after every
115
+ * `applySchema()`. Refuses (`unsafe_application_role`, with `reasons`) a role with a known way
116
+ * around forced RLS or the exclusion arbiter, directly, by membership or through PUBLIC
117
+ * (SECURITY.md lists the checks and what they do not cover). The check runs only at grant time.
132
118
  */
133
119
  grantApplicationRole(role: string, options?: SlotlockMigrationOptions): Promise<void>;
134
120
  /**
135
121
  * Run store operations with one transaction-local tenant context on the same connection. On a
136
- * client it opens a READ COMMITTED transaction, whatever the role's default. Inside a caller's
137
- * transaction of another level its writes are refused (`invalid_transaction_isolation`), since
138
- * that snapshot can miss a suspension, and its reads go on; with `isolation: 'read committed'`
139
- * such a transaction is refused outright. The serving role needs EXECUTE on
140
- * slotlock.tenant_suspended(text): without it this refuses with `application_role_not_granted`
141
- * (grantApplicationRole grants it), and on a schema without it with `schema_outdated`. With the
142
- * store's `maxTenantTransactions`, an aborted `signal` takes a call still waiting for its turn out
143
- * of the line, refused with the signal's reason.
122
+ * client it opens a READ COMMITTED transaction. Inside a caller's transaction of another level,
123
+ * writes are refused (`invalid_transaction_isolation`), since that snapshot can miss a
124
+ * suspension. Needs EXECUTE on slotlock.tenant_suspended(text) (`application_role_not_granted`
125
+ * without it; grantApplicationRole grants it). With `maxTenantTransactions`, an aborted `signal`
126
+ * removes a waiting call from the line.
144
127
  */
145
128
  withTenant<T>(tenantRef: string, callback: (store: SlotlockTenantStore) => Promise<T>, options?: {
146
129
  isolation?: 'read committed';
@@ -157,19 +140,23 @@ export interface SlotlockStore extends SlotlockEventFeed {
157
140
  origin?: SlotlockChangeOrigin;
158
141
  /** The id (a UUID) of the API key making these changes, recorded with each. */
159
142
  keyId?: string;
143
+ /**
144
+ * A key limited to part of its tenant (RFC 0003 G6): the transaction reaches only the
145
+ * resources at these locations (each with every location in it) or in these groups, as they
146
+ * are when it starts, by the database's resource_scope_v1 policies, on every table keyed by
147
+ * resource, for reads and writes. A resource outside it is not found.
148
+ */
149
+ resourceScope?: SlotlockResourceScope;
160
150
  }): Promise<T>;
161
151
  /**
162
152
  * INVARIANT (erasure completeness): a tenant integration must use a stable, erasable business
163
153
  * identifier for externalRef and include this schema in its own data-erasure workflow.
164
154
  *
165
- * With `maxTenantResources` (1-100,000, tenant-owned resources only), or the store's `resources`
166
- * limit (the lower of the two when both are set), a new resource of a tenant is refused
167
- * (`resource_limit_reached`, with the `limit`) once the tenant has that many; an existing
168
- * `externalRef` is still updated. Capped creates for one tenant run one at a time under a database lock, so racing ones
169
- * on any number of servers stop at the cap. The count needs READ COMMITTED: a capped create opens
170
- * one itself, and inside a caller's transaction of another level it refuses
171
- * (`invalid_transaction_isolation`). Uncapped creates do not wait for the lock and are not
172
- * counted against a cap until they commit.
155
+ * With `maxTenantResources` (1-100,000) or the store's `resources` limit (the lower applies), a
156
+ * new resource is refused (`resource_limit_reached`) once the tenant has that many; an existing
157
+ * `externalRef` is still updated. Capped creates serialize on a database lock, so racing servers
158
+ * stop at the cap; this needs READ COMMITTED (`invalid_transaction_isolation` inside another
159
+ * level).
173
160
  */
174
161
  createResource(params: {
175
162
  externalRef?: string;
@@ -182,7 +169,103 @@ export interface SlotlockStore extends SlotlockEventFeed {
182
169
  * refused (`resource_visibility_conflict`).
183
170
  */
184
171
  visibility?: SlotlockResource['visibility'];
172
+ /**
173
+ * Its profile (resource-profile.ts): name, description, kind, capacity, labels and attributes,
174
+ * each optional. Creating an existing reference again sets the fields given and keeps the rest.
175
+ * Refused (`invalid_resource_profile`) outside its bounds, or for a resource with no tenant.
176
+ */
177
+ profile?: SlotlockResourceProfileInput;
178
+ /**
179
+ * The reference of the location it is at (`location_not_found` when the tenant has none such).
180
+ * Without a `timezone`, a new resource takes the nearest time zone its location, or one above
181
+ * it, has, else UTC.
182
+ */
183
+ location?: string;
185
184
  }): Promise<SlotlockResource>;
185
+ /**
186
+ * Change one of the tenant's resources' profile: the fields given, keeping the rest (`null`
187
+ * clears a name, description or capacity). Null for an id that is not the tenant's; refused
188
+ * (`invalid_resource_profile`) outside its bounds. The feed's `resource.updated` names the
189
+ * fields that changed (`profile_changed`), never their values.
190
+ */
191
+ updateResourceProfile(params: {
192
+ tenantRef: string;
193
+ id: string;
194
+ profile: SlotlockResourceProfileInput;
195
+ /** Move it to this location, or to none (`null`); left out, it stays (`location_not_found`). */
196
+ location?: string | null;
197
+ }): Promise<SlotlockResource | null>;
198
+ /**
199
+ * The tenant's locations, each under its parent, depth first (RFC 0003 G2), with how many
200
+ * resources are at each.
201
+ */
202
+ listLocations(params: {
203
+ tenantRef: string;
204
+ }): Promise<SlotlockLocation[]>;
205
+ /**
206
+ * Create the location with this reference, or change its name, and the parent and time zone
207
+ * given. Refused: `invalid_location` (naming the field), `location_not_found` (no such parent),
208
+ * `location_too_deep` (past SLOTLOCK_MAX_LOCATION_DEPTH levels), `location_cycle` (under itself),
209
+ * `location_limit_reached` (SLOTLOCK_MAX_LOCATIONS).
210
+ */
211
+ putLocation(params: {
212
+ tenantRef: string;
213
+ ref: string;
214
+ name: string;
215
+ parentRef?: string | null;
216
+ timezone?: string | null;
217
+ }): Promise<SlotlockLocation>;
218
+ /** Change a location's name, parent or time zone, as putLocation; null when it has none such. */
219
+ updateLocation(params: {
220
+ tenantRef: string;
221
+ ref: string;
222
+ name?: string;
223
+ parentRef?: string | null;
224
+ timezone?: string | null;
225
+ }): Promise<SlotlockLocation | null>;
226
+ /**
227
+ * Delete a location: false when it has none such; refused (`location_in_use`) while a resource is
228
+ * at it or a location is under it.
229
+ */
230
+ deleteLocation(params: {
231
+ tenantRef: string;
232
+ ref: string;
233
+ }): Promise<boolean>;
234
+ /** The tenant's groups (RFC 0003 G3), by reference, with how many resources are in each. */
235
+ listGroups(params: {
236
+ tenantRef: string;
237
+ }): Promise<SlotlockResourceGroup[]>;
238
+ /**
239
+ * Create the group with this reference, or change its name, and its description when given.
240
+ * Refused: `invalid_group` (naming the field), `group_limit_reached` (SLOTLOCK_MAX_RESOURCE_GROUPS).
241
+ */
242
+ putGroup(params: {
243
+ tenantRef: string;
244
+ ref: string;
245
+ name: string;
246
+ description?: string | null;
247
+ }): Promise<SlotlockResourceGroup>;
248
+ /** Delete a group, its resources left as they are: false when it has none such. */
249
+ deleteGroup(params: {
250
+ tenantRef: string;
251
+ ref: string;
252
+ }): Promise<boolean>;
253
+ /**
254
+ * Put one of the tenant's resources in a group: true when it was not in it. Refused:
255
+ * `group_not_found`, `resource_not_found`, `resource_group_limit_reached`
256
+ * (SLOTLOCK_MAX_GROUPS_PER_RESOURCE).
257
+ */
258
+ addGroupMember(params: {
259
+ tenantRef: string;
260
+ group: string;
261
+ resourceId: string;
262
+ }): Promise<boolean>;
263
+ /** Take a resource out of a group: true when it was in it. */
264
+ removeGroupMember(params: {
265
+ tenantRef: string;
266
+ group: string;
267
+ resourceId: string;
268
+ }): Promise<boolean>;
186
269
  /**
187
270
  * Resolve one tenant-owned resource without revealing another tenant's matching id. An archived
188
271
  * one is returned (with `archivedAt`); with `visibility`, only one of that visibility.
@@ -273,15 +356,55 @@ export interface SlotlockStore extends SlotlockEventFeed {
273
356
  tenantRef: string;
274
357
  pool: string;
275
358
  visibility?: 'shared';
359
+ /** In the pool's order (`setPoolOrder`) rather than by id. */
360
+ ordered?: boolean;
361
+ /**
362
+ * For a booking about to take one (with `ordered`): a round-robin pool's place is locked until
363
+ * the transaction ends, so bookings at once go round in turn instead of starting from one place.
364
+ */
365
+ forBooking?: boolean;
276
366
  }): Promise<SlotlockResource[]>;
277
- /** The tenant's pools by name, with how many members each has that may be booked (its capacity). */
367
+ /**
368
+ * The tenant's pools by name, with how many members each has that may be booked (its capacity)
369
+ * and the order a pool booking tries them in.
370
+ */
278
371
  listPools(params: {
279
372
  tenantRef: string;
280
373
  visibility?: 'shared';
281
374
  }): Promise<{
282
375
  pool: string;
283
376
  capacity: number;
377
+ order: SlotlockPoolOrder;
284
378
  }[]>;
379
+ /**
380
+ * Set the order a pool booking tries a pool's members in (RFC 0003 G7): `stable` (by id, the
381
+ * default), `least_booked` or `round_robin`: `invalid_pool` for a name that is not a pool's,
382
+ * `invalid_pool_order` for an order that is not one of these, `pool_order_limit_reached` past
383
+ * SLOTLOCK_MAX_POOL_SETTINGS pools a tenant.
384
+ */
385
+ setPoolOrder(params: {
386
+ tenantRef: string;
387
+ pool: string;
388
+ order: SlotlockPoolOrder;
389
+ }): Promise<SlotlockPoolOrder>;
390
+ /**
391
+ * Make a resource a part of another (RFC 0003 G8), or no part (`null`): a booking of the whole
392
+ * then conflicts with every part, two parts never with each other. Null for no such resource;
393
+ * `parent_not_found`, `composite_too_deep` (one level only), `composite_parts_limit`
394
+ * (SLOTLOCK_MAX_COMPOSITE_PARTS) and `composite_busy` (either side has a booking or live hold to
395
+ * come) refuse it.
396
+ */
397
+ setResourceParent(params: {
398
+ tenantRef: string;
399
+ id: string;
400
+ parentId: string | null;
401
+ }): Promise<SlotlockResource | null>;
402
+ /** Note the member a pool booking took, from which `round_robin` goes on. */
403
+ notePoolBooking(params: {
404
+ tenantRef: string;
405
+ pool: string;
406
+ resourceId: string;
407
+ }): Promise<void>;
285
408
  /** Set (or clear, with `null`) the bookable hours of every resource the tenant has; returns how many. */
286
409
  setTenantAvailability(params: {
287
410
  tenantRef: string;
@@ -296,15 +419,49 @@ export interface SlotlockStore extends SlotlockEventFeed {
296
419
  visibility?: SlotlockResource['visibility'];
297
420
  /** Archived resources too; omitted, only those in use. */
298
421
  includeArchived?: boolean;
422
+ /** Only resources of this kind (`other` for those that say none). */
423
+ kind?: SlotlockResourceKind;
424
+ /** Only resources with every one of these labels (at most 20). */
425
+ labels?: readonly string[];
426
+ /** Only resources at this location or a location under it. */
427
+ location?: string;
428
+ /** Only resources in this group. */
429
+ group?: string;
430
+ /** Only this pool's members. */
431
+ pool?: string;
299
432
  }): Promise<SlotlockResource[]>;
300
433
  /**
301
- * Insert a confirmed reservation with the EXCLUDE constraint as the overlap arbiter. Never
302
- * throws for the expected outcomes: an invalid window or a lost race come back structured.
303
- * Expired holds overlapping the window are lazily reaped in the same transaction — an
304
- * expired-but-unpurged hold can never block (no cron required). On a tenant's resource the
305
- * booking takes the tenant, and its own id as reference, so cancel and reschedule reach it. With
306
- * `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
307
- * connection's row-level security).
434
+ * How many resources the tenant has, archived ones included: the count a resource limit (a hosted
435
+ * plan's, `maxTenantResources`) is checked against.
436
+ */
437
+ countResources(params: {
438
+ tenantRef: string;
439
+ }): Promise<number>;
440
+ /**
441
+ * Roll the tenant's committed bookings and holds up from its feed into its daily usage
442
+ * (usage.ts, `slotlock.roll_up_usage`), up to `limit` changes (1-100,000, default 5,000): how
443
+ * many it read. Fewer than `limit` means it has caught up.
444
+ */
445
+ rollUpUsage(params: {
446
+ tenantRef: string;
447
+ limit?: number;
448
+ }): Promise<number>;
449
+ /**
450
+ * The tenant's usage from `from` to `to`, UTC days `YYYY-MM-DD` inclusive, at most
451
+ * SLOTLOCK_USAGE_RETENTION_DAYS apart (usage.ts, `slotlock.tenant_usage`): empty unless the
452
+ * deployment meters it.
453
+ */
454
+ readUsage(params: {
455
+ tenantRef: string;
456
+ from: string;
457
+ to: string;
458
+ }): Promise<SlotlockUsageRow[]>;
459
+ /**
460
+ * Insert a confirmed reservation with the EXCLUDE constraint as the overlap arbiter; expected
461
+ * outcomes (invalid window, lost race) come back structured. Expired holds overlapping the window
462
+ * are reaped in the same transaction, so they never block. On a tenant's resource the booking
463
+ * takes the tenant and its own id as reference. With `tenantRef`, only that tenant's resource
464
+ * (`resource_not_found` otherwise, whatever the connection's RLS).
308
465
  */
309
466
  createReservation(params: {
310
467
  resourceId: string;
@@ -328,11 +485,9 @@ export interface SlotlockStore extends SlotlockEventFeed {
328
485
  revision: number;
329
486
  }): Promise<CancelExternalReservationResult>;
330
487
  /**
331
- * Occupy a window WITHOUT committing: the hold blocks rivals under the same EXCLUDE arbiter
332
- * until `expiresAt` (DB clock — the statement's time + ttl, never the app clock), then frees via
333
- * lazy reap.
334
- * With `tenantRef`, only a resource of that tenant (`resource_not_found` otherwise, whatever the
335
- * connection's row-level security).
488
+ * Occupy a window without committing: the hold blocks rivals under the same EXCLUDE arbiter until
489
+ * `expiresAt` (database clock: statement time + ttl), then frees via lazy reap. With `tenantRef`,
490
+ * only that tenant's resource (`resource_not_found` otherwise, whatever the connection's RLS).
336
491
  */
337
492
  acquireHold(params: {
338
493
  resourceId: string;
@@ -344,23 +499,28 @@ export interface SlotlockStore extends SlotlockEventFeed {
344
499
  /** Turnaround kept busy after `end`, 0 to 30 days; the confirmed booking keeps it. */
345
500
  bufferAfterMs?: number;
346
501
  /**
347
- * A retry identity, 1-500 UTF-8 bytes, unique inside `tenantRef` and `principalRef` (both
348
- * required with it): a request repeated with the same key and body answers the hold the first
349
- * made (`idempotent: true`), and one with another body is `idempotency_conflict`. The answer is
350
- * kept until the hold would have expired, released or confirmed; a retry after that makes a new
351
- * hold.
502
+ * A retry identity, 1-500 UTF-8 bytes, unique within `tenantRef` and `principalRef` (both
503
+ * required): the same key and body answer the first hold (`idempotent: true`); another body is
504
+ * `idempotency_conflict`. Kept until the hold would have expired, released or confirmed.
352
505
  */
353
506
  idempotencyKey?: string;
354
507
  /** Whose keys `idempotencyKey` is among, 1-500 UTF-8 bytes (an API key's subject, say). */
355
508
  principalRef?: string;
356
509
  /**
357
- * Who the hold belongs to, 1-500 UTF-8 bytes, inside `tenantRef` (required with it): an agent
358
- * acting for one person, say. `releaseHold` and `putCalendarEvent`'s `fromHold` with a
359
- * `holderRef` take only their own, and a holder may have `maxHoldsPerHolder` (20) live at once,
360
- * holding at most `maxHeldHoursPerHolder` together when that is set. Without it the hold has no
361
- * holder and no cap: an application holding for its customers.
510
+ * Who the hold belongs to, 1-500 UTF-8 bytes, within `tenantRef` (required): `releaseHold` and
511
+ * `fromHold` with a `holderRef` take only their own, and a holder may have `maxHoldsPerHolder`
512
+ * live at once (and `maxHeldHoursPerHolder` together, when set). Without it the hold has no
513
+ * holder and no cap.
362
514
  */
363
515
  holderRef?: string;
516
+ /**
517
+ * Lower the holder's caps for this hold (an API key's own limits, RFC 0004 A5): never above
518
+ * `maxHoldsPerHolder` and `maxHeldHoursPerHolder`, which win where they are lower.
519
+ */
520
+ holderCaps?: {
521
+ maxHolds?: number;
522
+ maxHeldHours?: number;
523
+ };
364
524
  /**
365
525
  * Holds taken as alternatives for one booking share a group (a UUID): booking one with
366
526
  * `fromHold` releases the rest, and releasing one with a holder releases them all.
@@ -372,6 +532,18 @@ export interface SlotlockStore extends SlotlockEventFeed {
372
532
  tenantRef: string;
373
533
  holdId: string;
374
534
  }): Promise<SlotlockHold | null>;
535
+ /**
536
+ * One holder's own hold by id, lapsed or live (`live`), so a confirm can say `hold_expired`
537
+ * rather than not found. Null when it is gone, booked, or another holder's.
538
+ */
539
+ getHolderHold(params: {
540
+ tenantRef: string;
541
+ holdId: string;
542
+ holderRef: string;
543
+ }): Promise<{
544
+ hold: SlotlockHold;
545
+ live: boolean;
546
+ } | null>;
375
547
  /** The tenant's live holds on one resource, by start then id, keyset-paginated like events. */
376
548
  listHolds(params: {
377
549
  tenantRef: string;
@@ -401,6 +573,156 @@ export interface SlotlockStore extends SlotlockEventFeed {
401
573
  tenantRef: string;
402
574
  externalRef: string;
403
575
  }): Promise<SlotlockReservation | null>;
576
+ /**
577
+ * Any booking, hold or event occurrence of the tenant's by its row id (the id a refused write's
578
+ * `conflictingReservationId` names): the time it occupies, buffers included, and whose it is.
579
+ * Null when the tenant has none, or the transaction's key may not see it.
580
+ */
581
+ describeBooking(params: {
582
+ tenantRef: string;
583
+ id: string;
584
+ }): Promise<SlotlockBookingDescription | null>;
585
+ /**
586
+ * The time an owner's calendar events occupy now, buffers included, one row per occupation: what
587
+ * an approval (RFC 0004 A4) holds for a write, read in the write's own rolled-back transaction.
588
+ */
589
+ eventOccupancy(params: {
590
+ tenantRef: string;
591
+ ownerRef: string;
592
+ externalRefs: readonly string[];
593
+ }): Promise<{
594
+ resourceId: string;
595
+ start: Date;
596
+ end: Date;
597
+ recurring: boolean;
598
+ }[]>;
599
+ /** Create or replace an approval policy; null when the tenant has its 50 already. */
600
+ putApprovalPolicy(params: PutApprovalPolicyParams): Promise<SlotlockApprovalPolicy | null>;
601
+ listApprovalPolicies(params: {
602
+ tenantRef: string;
603
+ }): Promise<SlotlockApprovalPolicy[]>;
604
+ deleteApprovalPolicy(params: {
605
+ tenantRef: string;
606
+ ref: string;
607
+ }): Promise<{
608
+ deleted: boolean;
609
+ }>;
610
+ /** The tenant's policies that apply to any of these resources (by id, pool, location, group, or all). */
611
+ approvalPoliciesFor(params: {
612
+ tenantRef: string;
613
+ resourceIds: readonly string[];
614
+ }): Promise<SlotlockApprovalPolicy[]>;
615
+ /**
616
+ * A write waiting for a person: its time held by holds the approval owns, until `expiresAt`, and
617
+ * the request kept to run as its requester when granted. A retry with the same `requestKey`
618
+ * answers the same approval (`replayed`), or `idempotency_conflict` for another request. With
619
+ * `consumeHold`, the requester's own hold of that time gives way to the approval's in the same
620
+ * transaction. `overlap` when the time is taken; `approval_limit_reached` past the pending caps.
621
+ */
622
+ requestApproval(params: {
623
+ tenantRef: string;
624
+ policyRef: string;
625
+ operation: SlotlockApproval['operation'];
626
+ request: Record<string, unknown>;
627
+ requester: Record<string, unknown>;
628
+ requesterRef: string;
629
+ requestKey: string;
630
+ holds: readonly {
631
+ resourceId: string;
632
+ start: Date;
633
+ end: Date;
634
+ }[];
635
+ start: Date;
636
+ end: Date;
637
+ expiresAt: Date;
638
+ consumeHold?: {
639
+ holdId: string;
640
+ holderRef: string;
641
+ };
642
+ }): Promise<{
643
+ ok: true;
644
+ approval: SlotlockApproval;
645
+ replayed: boolean;
646
+ } | {
647
+ ok: false;
648
+ code: 'overlap' | 'approval_limit_reached' | 'idempotency_conflict';
649
+ limit?: number;
650
+ }>;
651
+ /** The approval a request key made, or null. */
652
+ findApproval(params: {
653
+ tenantRef: string;
654
+ requestKey: string;
655
+ }): Promise<SlotlockApproval | null>;
656
+ getApproval(params: {
657
+ tenantRef: string;
658
+ id: string;
659
+ }): Promise<(SlotlockApproval & {
660
+ requester: Record<string, unknown>;
661
+ }) | null>;
662
+ listApprovals(params: {
663
+ tenantRef: string;
664
+ status?: SlotlockApproval['status'];
665
+ limit?: number;
666
+ }): Promise<SlotlockApproval[]>;
667
+ /**
668
+ * Lock a pending, unexpired approval and let its holds go, so its write can take their time in
669
+ * the same transaction; finishApprovalGrant then marks it granted.
670
+ */
671
+ beginApprovalGrant(params: {
672
+ tenantRef: string;
673
+ id: string;
674
+ }): Promise<{
675
+ ok: true;
676
+ approval: SlotlockApproval;
677
+ } | {
678
+ ok: false;
679
+ code: 'approval_not_found' | 'approval_not_pending' | 'approval_expired';
680
+ }>;
681
+ finishApprovalGrant(params: {
682
+ tenantRef: string;
683
+ id: string;
684
+ decidedBy: string;
685
+ }): Promise<SlotlockApproval>;
686
+ declineApproval(params: {
687
+ tenantRef: string;
688
+ id: string;
689
+ decidedBy: string;
690
+ reason?: string;
691
+ }): Promise<{
692
+ ok: true;
693
+ approval: SlotlockApproval;
694
+ } | {
695
+ ok: false;
696
+ code: 'approval_not_found' | 'approval_not_pending' | 'approval_expired';
697
+ }>;
698
+ /** Maintenance: up to `limit` pending approvals past their wait expire and free their time. */
699
+ expireApprovals(params: {
700
+ tenantRef: string;
701
+ limit?: number;
702
+ }): Promise<{
703
+ expired: number;
704
+ }>;
705
+ /**
706
+ * Empty a test environment (RFC 0004 A6): its resources, locations, groups, bookings, events,
707
+ * holds, waitlist, approval policies and feed; its keys, webhooks, plan and usage stay. Refused
708
+ * (`sandbox_test_only`) for a tenant that is not a test environment (`~test`). Rows removed, by
709
+ * table.
710
+ */
711
+ resetTestTenant(params: {
712
+ tenantRef: string;
713
+ }): Promise<Record<string, number>>;
714
+ /**
715
+ * In a test environment, make what would lapse within `minutes` lapse now (RFC 0004 A6): holds
716
+ * (a waitlist offer's and an approval's among them) and pending approvals. Maintenance then
717
+ * reaps them as it would have. Refused (`sandbox_test_only`) elsewhere.
718
+ */
719
+ fastForwardTestTenant(params: {
720
+ tenantRef: string;
721
+ minutes: number;
722
+ }): Promise<{
723
+ holds: number;
724
+ approvals: number;
725
+ }>;
404
726
  /**
405
727
  * The tenant's bookings on one resource whose occupied time meets `window` (at most 367 days),
406
728
  * by start then id, keyset-paginated: pass the last one's `{ start, id }` as `after`.
@@ -416,12 +738,10 @@ export interface SlotlockStore extends SlotlockEventFeed {
416
738
  };
417
739
  }): Promise<SlotlockReservation[]>;
418
740
  /**
419
- * Flip an unexpired hold to a confirmed booking (expiry cleared). On a tenant's resource the
420
- * booking takes the tenant, and the hold's id as its reference at revision 1, so cancel and
421
- * reschedule reach it like any other (`reservation_cancelled` when a cancel of that reference
422
- * came first). Confirming it again answers the booking as it now is. With `tenantRef`, only a
423
- * hold on one of that tenant's resources counts: another tenant's is `hold_not_found` whatever
424
- * the connection's row-level security.
741
+ * Turn an unexpired hold into a confirmed booking. On a tenant's resource the booking takes the
742
+ * tenant and the hold's id as reference at revision 1 (`reservation_cancelled` when a cancel of
743
+ * that reference came first). Confirming again answers the booking as it now is. With
744
+ * `tenantRef`, another tenant's hold is `hold_not_found` whatever the connection's RLS.
425
745
  */
426
746
  confirmHold(holdId: string, options?: SlotlockHoldTenantOptions): Promise<ConfirmHoldResult>;
427
747
  /**
@@ -511,12 +831,10 @@ export interface SlotlockStore extends SlotlockEventFeed {
511
831
  expectedResourceId?: string;
512
832
  }): Promise<CancelCalendarEventResult>;
513
833
  /**
514
- * Atomically rematerialize due recurring masters into one rolling window without changing their
515
- * semantic revision. A collision rolls back that master and leaves its old coverage untouched.
516
- * A master is rolled from the window's start through the later of its stored end and the
517
- * window's, so its end never moves back. One that already covers the window, or whose end lies
518
- * too far past it for the 367-day cap from the window's start, is not due: it is left as it is
519
- * and not counted.
834
+ * Rematerialize due recurring masters into one rolling window without changing their revision. A
835
+ * collision rolls back that master and keeps its old coverage. A master is rolled from the
836
+ * window's start through the later of its stored end and the window's; one already covering the
837
+ * window, or too far past it for the 367-day cap, is not due and not counted.
520
838
  */
521
839
  rollCalendarEventHorizon(params: {
522
840
  tenantRef: string;
@@ -526,12 +844,10 @@ export interface SlotlockStore extends SlotlockEventFeed {
526
844
  after?: CalendarHorizonRollCursor;
527
845
  }): Promise<CalendarHorizonRollResult>;
528
846
  /**
529
- * Delete one bounded batch of expired event bookkeeping for a tenant, which is what returns agent
530
- * quota: idempotency commands older than the replay window (every owner), then agent-owned
531
- * cancellation tombstones older than it with no remaining command. Active events and `internal`
532
- * (provider/sync) tombstones are never deleted. Rows another transaction holds are skipped, not
533
- * waited on. After the window a retried command is evaluated afresh under its expected revision,
534
- * and a pruned agent identity can be created again. Call until `hasMore` is false.
847
+ * Delete one bounded batch of expired event bookkeeping, which returns agent quota: commands
848
+ * older than the replay window (every owner), then agent-owned tombstones older than it with no
849
+ * remaining command. Active events and `internal` tombstones are never deleted. Locked rows are
850
+ * skipped. Afterwards a retried command is evaluated afresh. Call until `hasMore` is false.
535
851
  */
536
852
  pruneCalendarEventRetention(params: {
537
853
  tenantRef: string;
@@ -541,21 +857,17 @@ export interface SlotlockStore extends SlotlockEventFeed {
541
857
  retentionDays?: number;
542
858
  }): Promise<CalendarEventRetentionResult>;
543
859
  /**
544
- * Delete one bounded batch of a tenant's dead booking rows: holds past their expiry and the
545
- * answers kept for keyed ones (at once), and references retired by a cancel no booking held,
546
- * older than the retention window. A cancelled booking's tombstone is never deleted here (only by
547
- * erasing the tenant), so its reference is never booked again. Rows another transaction holds
548
- * are skipped, not waited on. It also gives a confirmed booking a 0.2 server wrote without its
549
- * tenant the tenant and its own id as reference, as the migration does. Call until `hasMore` is
550
- * false.
860
+ * Delete one bounded batch of a tenant's dead booking rows: expired holds and their keyed
861
+ * answers, and references retired with no booking past retention. A cancelled booking's tombstone
862
+ * is only deleted by erasure, so its reference is never booked again. Locked rows are skipped.
863
+ * Also gives a confirmed booking a 0.2 server wrote without a tenant its tenant and reference, as
864
+ * the migration does. Call until `hasMore` is false.
551
865
  */
552
866
  /**
553
- * Time zone rules that changed since an event was booked (a new tzdata): a future one-off event
554
- * whose wall-clock time in its zone now falls at another instant. Each is reported once on the
555
- * feed (`event.timezone_shifted`, with the instant it has and the one its wall time now means).
556
- * With `move`, it is moved there instead (a new revision, `event.updated` naming `slotlock:tzdata`),
557
- * unless that overlaps another booking: then it stays and `event.timezone_conflict` reports it,
558
- * for a person or an agent to decide. Up to `limit` (1-1,000, default 100) events a call.
867
+ * Find future one-off events whose wall-clock time now falls at another instant (new tzdata),
868
+ * each reported once (`event.timezone_shifted`). With `move`, it is moved (a new revision,
869
+ * `event.updated` by `slotlock:tzdata`) unless that overlaps another booking, which
870
+ * `event.timezone_conflict` reports instead. Up to `limit` (1-1,000, default 100) a call.
559
871
  */
560
872
  followZoneRules(params: {
561
873
  tenantRef: string;
@@ -644,6 +956,11 @@ export interface SlotlockStore extends SlotlockEventFeed {
644
956
  end: Date;
645
957
  holderRef: string;
646
958
  offerTtlMs?: number;
959
+ /**
960
+ * A retry identity, kept as long as the entry: the same key and request answer the entry the
961
+ * first made (`replayed`), another request `idempotency_conflict`.
962
+ */
963
+ idempotencyKey?: string;
647
964
  }): Promise<JoinWaitlistResult>;
648
965
  /** The tenant's waitlist entries, oldest first, optionally of one resource, pool or holder. */
649
966
  listWaitlist(params: {
@@ -690,6 +1007,17 @@ export interface SlotlockStore extends SlotlockEventFeed {
690
1007
  deleteAssistantSettings(params: {
691
1008
  tenantRef: string;
692
1009
  }): Promise<boolean>;
1010
+ /**
1011
+ * Replace the tenant's sealed assistant key `from` with `to`, the same key sealed under a newer
1012
+ * secret (SLOTLOCK_ASSISTANT_PREVIOUS_SECRETS), only while `from` is still the one stored: a key an
1013
+ * owner saved since is never overwritten. Nothing else changes, its hint and who set it included.
1014
+ * True when it was replaced.
1015
+ */
1016
+ resealAssistantKey(params: {
1017
+ tenantRef: string;
1018
+ from: string;
1019
+ to: string;
1020
+ }): Promise<boolean>;
693
1021
  /** A member's live assistant conversation, or null when none is (or it expired). */
694
1022
  getAssistantConversation(params: {
695
1023
  tenantRef: string;
@@ -740,12 +1068,11 @@ export interface SlotlockStore extends SlotlockEventFeed {
740
1068
  limit?: number;
741
1069
  }): Promise<number>;
742
1070
  /**
743
- * Offer freed time to the waitlist, in the order entries joined: each waiting entry whose window
744
- * is free (on its resource, or on the first free member of its pool) gets a hold for its holder
745
- * and the feed says so (`waitlist.offered`, with the hold). Entries whose window has started are
746
- * expired (`waitlist.expired`); finished ones older than the change feed's retention are deleted.
747
- * One tenant's offers run one at a time. One page of `limit` (1-100, default 50) entries: pass
748
- * `next` back as `after` for the next, until it is null.
1071
+ * Offer freed time to the waitlist in join order: each waiting entry whose window is free (on its
1072
+ * resource, or the first free pool member) gets a hold for its holder (`waitlist.offered`).
1073
+ * Started windows expire (`waitlist.expired`); finished entries past retention are deleted. One
1074
+ * tenant's offers run one at a time. Pages of `limit` (1-100, default 50): pass `next` back as
1075
+ * `after` until null.
749
1076
  */
750
1077
  offerWaitlist(params: {
751
1078
  tenantRef: string;
@@ -773,11 +1100,10 @@ export interface SlotlockStore extends SlotlockEventFeed {
773
1100
  retentionDays?: number;
774
1101
  }): Promise<BookingRetentionResult>;
775
1102
  /**
776
- * Suspend a tenant: from when this returns, every withTenant transaction for it (as every server
777
- * runs its requests) may read, and its writes are refused (`tenant_suspended`); one in flight is
778
- * waited for. Its keys still authenticate, but it gets no new key or secret. Maintenance and
779
- * erasure go on. True when it was not suspended already. Only the deployment role, which owns the
780
- * schema, may suspend (`tenant_suspension_not_permitted` for another).
1103
+ * Suspend a tenant: from when this returns, its withTenant transactions may read but writes are
1104
+ * refused (`tenant_suspended`); one in flight is waited for. Its keys still authenticate but it
1105
+ * gets no new key. Maintenance and erasure go on. True when it was not suspended already.
1106
+ * Deployment role only (`tenant_suspension_not_permitted`).
781
1107
  */
782
1108
  suspendTenant(params: {
783
1109
  tenantRef: string;
@@ -797,11 +1123,9 @@ export interface SlotlockStore extends SlotlockEventFeed {
797
1123
  tenantRef: string;
798
1124
  }): Promise<boolean>;
799
1125
  /**
800
- * Erase a tenant: delete every row it owns (resources, holds, bookings, events with their
801
- * attendees and reminders, cancellations, idempotency records, coverage, API keys, and a personal
802
- * tenant's dashboard records) in one transaction, waiting for its transactions in flight. API key
803
- * digests are retired so no erased key works again; a suspension stays. Erasing again deletes
804
- * nothing.
1126
+ * Erase a tenant: delete every row it owns in one transaction, waiting for its transactions in
1127
+ * flight. API key digests are retired so no erased key works again; a suspension stays. Erasing
1128
+ * again deletes nothing.
805
1129
  */
806
1130
  eraseTenant(params: {
807
1131
  tenantRef: string;
@@ -817,12 +1141,11 @@ export interface SlotlockStore extends SlotlockEventFeed {
817
1141
  coverageMaxAgeMs?: number;
818
1142
  }): Promise<CalendarFreeBusy>;
819
1143
  /**
820
- * The first slot of `durationMs` inside the hours, whose time and turnaround buffer are free:
821
- * getFreeBusy × expandRules (in the resource's stored timezone) → findNextAvailable. Null when no
822
- * slot is free, and when one cannot be proven free: the coverage of the window (with the buffer's
823
- * tail) is not complete, because a recurring series is not stored that far or a required provider
824
- * calendar is not read that far. Another tenant's resource, an unknown id or one the store never
825
- * issues is `resource_not_found`. A search window longer than 367 days is `invalid_window`.
1144
+ * The first slot of `durationMs` inside the hours whose time and turnaround buffer are free
1145
+ * (getFreeBusy × expandRules in the resource's timezone → findNextAvailable). Null when no slot
1146
+ * is free or one cannot be proven free (coverage incomplete: a series not stored that far, or a
1147
+ * required provider not read that far). Unknown or another tenant's resource is
1148
+ * `resource_not_found`; a window over 367 days is `invalid_window`.
826
1149
  */
827
1150
  findNextAvailableFor(params: {
828
1151
  /** The resource's tenant; `null` for a resource that has none (the legacy namespace). */
@@ -882,9 +1205,10 @@ export interface SlotlockWaitlistEntry {
882
1205
  export type JoinWaitlistResult = {
883
1206
  ok: true;
884
1207
  entry: SlotlockWaitlistEntry;
1208
+ replayed: boolean;
885
1209
  } | {
886
1210
  ok: false;
887
- code: 'invalid_identity' | 'invalid_window' | 'invalid_ttl' | 'invalid_pool' | 'resource_not_found' | 'resource_archived' | 'pool_not_found' | 'waitlist_limit_reached';
1211
+ code: 'invalid_identity' | 'invalid_window' | 'invalid_ttl' | 'invalid_pool' | 'resource_not_found' | 'resource_archived' | 'pool_not_found' | 'waitlist_limit_reached' | 'idempotency_conflict';
888
1212
  };
889
1213
  export type CheckInReservationResult = {
890
1214
  ok: true;
@@ -926,20 +1250,16 @@ export interface SlotlockStoreOptions {
926
1250
  /** Hard retained-command ceiling per non-internal owner. Defaults to 10,000; cannot be disabled. */
927
1251
  agentOwnerCommandQuota?: number;
928
1252
  /**
929
- * What one tenant may store, for a database tenants share (none by default). A write that would
930
- * pass a limit is refused with it (`*_limit_reached`); what the tenant has can still be revised
931
- * (a move adds no booking or event, so it is not refused), cancelled and released. Counted under
932
- * a lock per tenant and limit, so racing writers never pass it, in READ COMMITTED transactions
933
- * only (`invalid_transaction_isolation` otherwise). Bookings and events count while they are still
934
- * to come, and for 30 days after they were made, whatever time they hold; history older than that
935
- * does not count, and a count stops reading at the limit.
1253
+ * What one tenant may store, for a shared database (none by default). A write that would pass a
1254
+ * limit is refused (`*_limit_reached`); existing rows can still be revised, cancelled and
1255
+ * released. Counted under a per-tenant lock in READ COMMITTED transactions only. Bookings and
1256
+ * events count while still to come and for 30 days after they were made.
936
1257
  */
937
1258
  tenantLimits?: SlotlockTenantLimits;
938
1259
  /**
939
- * How many withTenant transactions of one tenant run at once on this store (1-1,000; no limit by
940
- * default). The rest wait in this process, in order, so one tenant's transactions waiting on its
941
- * own locks hold at most this many of a pool's connections. Not counted inside a caller's
942
- * transaction, which holds its connection already.
1260
+ * How many withTenant transactions of one tenant run at once on this store (1-1,000; unlimited by
1261
+ * default). The rest wait in order, so one tenant waiting on its own locks holds at most this
1262
+ * many pool connections. Not counted inside a caller's transaction.
943
1263
  */
944
1264
  maxTenantTransactions?: number;
945
1265
  /**
@@ -949,11 +1269,9 @@ export interface SlotlockStoreOptions {
949
1269
  */
950
1270
  tenantLockTimeoutMs?: number;
951
1271
  /**
952
- * How many days history is kept, 1-3,650 (default 30): the change feed (and how far back a cursor
953
- * reaches) and waitlist entries that ended; and, when longer than 30 days, retired idempotency
954
- * answers, booking references and past events' commands (never shorter, so a delayed retry is still
955
- * answered as the first try was). Maintenance prunes what is older. A cursor before a change any
956
- * store has pruned answers `cursor_expired`, so stores set differently never skip a change.
1272
+ * Days history is kept, 1-3,650 (default 30): the change feed (and cursor reach) and ended
1273
+ * waitlist entries; and, when longer than 30 days, retired idempotency answers, booking
1274
+ * references and past events' commands. A cursor before a pruned change answers `cursor_expired`.
957
1275
  */
958
1276
  retentionDays?: number;
959
1277
  /**
@@ -972,10 +1290,9 @@ export interface SlotlockTenantLimits {
972
1290
  /** Resources, archived ones included; the lower of this and a call's `maxTenantResources`. */
973
1291
  resources?: number;
974
1292
  /**
975
- * Holds on the tenant's resources, expired ones included until they are deleted. A keyed hold
976
- * keeps its place until it would have expired, released or confirmed, while its answer is kept
977
- * for retries. A hold at the limit first deletes the tenant's expired holds and forgets their
978
- * answers, as maintenance does, so only live holds keep a new one out.
1293
+ * Holds, expired ones included until deleted; a keyed hold keeps its place while its answer is
1294
+ * kept. At the limit, expired holds and their answers are deleted first, so only live holds keep
1295
+ * a new one out.
979
1296
  */
980
1297
  holds?: number;
981
1298
  /**
@@ -984,9 +1301,9 @@ export interface SlotlockTenantLimits {
984
1301
  */
985
1302
  reservations?: number;
986
1303
  /**
987
- * Calendar events of every owner still to come (a series until it is known to have ended) or made
988
- * in the last 30 days, cancellations of every owner made in the last 30 days, and agent-owned
989
- * cancellations until maintenance prunes them. Each event stores at most 2,000 occurrences.
1304
+ * Events of every owner still to come (a series until known to have ended) or made in the last 30
1305
+ * days, cancellations made in the last 30 days, and agent cancellations until pruned. Each event
1306
+ * stores at most 2,000 occurrences.
990
1307
  */
991
1308
  events?: number;
992
1309
  /** Provider coverage rows: one per resource and source. */
@@ -1006,4 +1323,4 @@ export declare function createSlotlockStore(sql: StoreSql, options?: SlotlockSto
1006
1323
  * Every store method that writes for a tenant: what a suspended tenant's withTenant store refuses.
1007
1324
  * Maintenance (rolling series, pruning, purging) and erasure are not among them.
1008
1325
  */
1009
- export declare const SLOTLOCK_TENANT_WRITE_METHODS: readonly ["createResource", "archiveResource", "restoreResource", "deleteResource", "setResourceAvailability", "setResourceBookingRules", "setBookingRuleDefaults", "setResourcePool", "setTenantAvailability", "createReservation", "createExternalReservation", "rescheduleExternalReservation", "cancelExternalReservation", "acquireHold", "confirmHold", "releaseHold", "putCalendarEvent", "cancelCalendarEvent", "cancelCalendarEventById", "recordCalendarCoverage", "createWebhookEndpoint", "deleteWebhookEndpoint", "enableWebhookEndpoint", "recordCoverageFailure", "deleteCoverage", "checkInReservation", "joinWaitlist", "leaveWaitlist", "putAssistantSettings", "deleteAssistantSettings", "putAssistantConversation", "settleAssistantAction", "deleteAssistantConversation"];
1326
+ export declare const SLOTLOCK_TENANT_WRITE_METHODS: readonly ["createResource", "updateResourceProfile", "putLocation", "updateLocation", "deleteLocation", "putGroup", "deleteGroup", "addGroupMember", "removeGroupMember", "archiveResource", "restoreResource", "deleteResource", "setResourceAvailability", "setResourceBookingRules", "setBookingRuleDefaults", "setResourcePool", "setResourceParent", "setPoolOrder", "notePoolBooking", "putApprovalPolicy", "deleteApprovalPolicy", "requestApproval", "beginApprovalGrant", "finishApprovalGrant", "declineApproval", "resetTestTenant", "fastForwardTestTenant", "setTenantAvailability", "createReservation", "createExternalReservation", "rescheduleExternalReservation", "cancelExternalReservation", "acquireHold", "confirmHold", "releaseHold", "putCalendarEvent", "cancelCalendarEvent", "cancelCalendarEventById", "recordCalendarCoverage", "createWebhookEndpoint", "deleteWebhookEndpoint", "enableWebhookEndpoint", "recordCoverageFailure", "deleteCoverage", "checkInReservation", "joinWaitlist", "leaveWaitlist", "putAssistantSettings", "deleteAssistantSettings", "resealAssistantKey", "putAssistantConversation", "settleAssistantAction", "deleteAssistantConversation"];