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
@@ -1,21 +1,107 @@
1
- // The application API: what an application, rather than an agent, does over HTTP that the TypeScript
2
- // store does in process. It creates resources and sets their bookable hours, holds and reserves time,
3
- // records how much of an external calendar it has read, reaches any of the tenant's events whichever
4
- // key wrote them, erases the tenant when allowed, and runs maintenance. Any language can call it.
1
+ // The application API: what an application, rather than an agent, does over HTTP that the
2
+ // TypeScript store does in process (resources and hours, holds and reservations, coverage, any of
3
+ // the tenant's events, erasure when allowed, maintenance), for any language.
5
4
  //
6
- // The agent server serves these operations as REST under /v1, beside the agent tools, when it is
7
- // given an `application` backend and `rest` is on, and describes them in the same OpenAPI document.
8
- // They need the `admin` scope. They are never MCP tools or A2A skills: no agent is offered them.
5
+ // Served as REST under /v1 beside the agent tools when the agent server has an `application`
6
+ // backend and `rest` is on, in the same OpenAPI document. Needs the `admin` scope; never MCP tools
7
+ // or A2A skills, so no agent is offered them.
9
8
  //
10
- // This module holds only the contract (names, schemas, routes); it imports nothing from the agent
11
- // server at runtime. The store-backed implementation is createSlotlockStoreApplicationBackend.
12
- import { SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_TYPES } from './event-feed.js';
9
+ // Contract only (names, schemas, routes); nothing from the agent server at runtime. The
10
+ // store-backed implementation is createSlotlockStoreApplicationBackend.
11
+ import { SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_WEBHOOK_FILTER_REFS, SLOTLOCK_WEBHOOK_FILTER_RESOURCES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, } from './event-feed.js';
13
12
  import { z } from 'zod';
13
+ import { SLOTLOCK_APPROVAL_KINDS, SLOTLOCK_MAX_APPROVAL_MINUTES, SLOTLOCK_MAX_APPROVAL_POLICIES, } from './approvals.js';
14
+ import { SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_PLAN } from './partner-names.js';
14
15
  import { SLOTLOCK_EVENT_OUTPUT } from './event-schema.js';
15
16
  import { SLOTLOCK_CALENDAR_HORIZON_DAYS } from './rules.js';
16
- /** The scope an application operation needs, whichever operation it is. */
17
+ import { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_POOL_ORDERS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_LIMITS, } from './resource-profile.js';
18
+ /** The scope an application operation needs, but the partner API's (`slotlockApplicationScope`). */
17
19
  export const SLOTLOCK_APPLICATION_SCOPE = 'admin';
20
+ /** The scope one application operation needs: `partner` for the partner API's, else `admin`. */
21
+ export function slotlockApplicationScope(definition) {
22
+ return definition.scope ?? SLOTLOCK_APPLICATION_SCOPE;
23
+ }
24
+ /**
25
+ * Writes that cannot be a dry run: they do not run in one tenant transaction that can be undone
26
+ * (erasure, maintenance's passes, the partner store's).
27
+ */
28
+ const NOT_DRY_RUNNABLE = new Set([
29
+ 'slotlock_erase_tenant',
30
+ 'slotlock_run_maintenance',
31
+ 'slotlock_sandbox_seed',
32
+ 'slotlock_sandbox_reset',
33
+ 'slotlock_sandbox_fast_forward',
34
+ 'slotlock_sandbox_race',
35
+ 'slotlock_partner_put_account',
36
+ 'slotlock_partner_create_key',
37
+ 'slotlock_partner_rotate_key',
38
+ 'slotlock_partner_revoke_key',
39
+ 'slotlock_partner_suspend_account',
40
+ 'slotlock_partner_unsuspend_account',
41
+ 'slotlock_partner_erase_account',
42
+ 'slotlock_partner_reconcile_usage',
43
+ ]);
44
+ /** Whether an application operation takes `dry_run` (RFC 0004 A3): a write that can be undone. */
45
+ export function slotlockApplicationDryRunnable(definition) {
46
+ return !definition.risk.readOnly && !NOT_DRY_RUNNABLE.has(definition.name);
47
+ }
18
48
  const DATE_TIME = z.string().datetime({ offset: true });
49
+ /** A UTC day, `YYYY-MM-DD`, that is a real date. */
50
+ const USAGE_DAY = z
51
+ .string()
52
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
53
+ .refine((day) => new Date(`${day}T00:00:00Z`).toISOString().startsWith(day), 'not a date');
54
+ const USAGE_COUNT = z.number().int().min(0);
55
+ const USAGE_TOTALS = {
56
+ operations: USAGE_COUNT,
57
+ bookings: USAGE_COUNT,
58
+ holds: USAGE_COUNT,
59
+ webhook_deliveries: USAGE_COUNT,
60
+ assistant_messages: USAGE_COUNT,
61
+ resources: USAGE_COUNT,
62
+ };
63
+ const PARTNER_ACCOUNT = z.string().regex(SLOTLOCK_PARTNER_ACCOUNT);
64
+ const PARTNER_PLAN_NAME = z.string().regex(SLOTLOCK_PARTNER_PLAN);
65
+ const PARTNER_ACCOUNT_OUTPUT = z
66
+ .object({
67
+ account: PARTNER_ACCOUNT,
68
+ plan: PARTNER_PLAN_NAME,
69
+ status: z.enum(['active', 'suspended']),
70
+ created_at: z.string().datetime(),
71
+ updated_at: z.string().datetime(),
72
+ resources: z.number().int().min(0).optional(),
73
+ api_keys: z.number().int().min(0).optional(),
74
+ })
75
+ .strict();
76
+ /**
77
+ * Which of an account's environments a key call acts in (RFC 0003 G9): `live`, the default, or
78
+ * `test`, the account's test environment, whose keys start `slk_test_` and are never billed.
79
+ */
80
+ const PARTNER_ENVIRONMENT = z.enum(['live', 'test']).optional();
81
+ const ACCOUNT_KEY = z.string().regex(/^slk_(test_)?[0-9A-Za-z]{46}$/);
82
+ /** An agent key held below the server's limits (RFC 0004 A5): each field only lowers. */
83
+ const KEY_LIMITS = z
84
+ .object({
85
+ max_holds: z.number().int().min(1).max(10_000).optional(),
86
+ max_held_hours: z.number().int().min(1).max(876_000).optional(),
87
+ requests_per_minute: z.number().int().min(1).max(100_000).optional(),
88
+ requires_approval: z.boolean().optional(),
89
+ })
90
+ .strict();
91
+ const PARTNER_KEY_OUTPUT = z
92
+ .object({
93
+ id: z.string().uuid(),
94
+ name: z.string().min(1).max(100),
95
+ prefix: z.string().min(1).max(20),
96
+ scopes: z.array(z.enum(['read', 'write', 'admin'])),
97
+ created_at: z.string().datetime(),
98
+ expires_at: z.string().datetime().nullable(),
99
+ last_used_at: z.string().datetime().nullable(),
100
+ revoked_at: z.string().datetime().nullable(),
101
+ /** What the key may do below the server's limits (RFC 0004 A5); null for the server's. */
102
+ limits: KEY_LIMITS.nullable(),
103
+ })
104
+ .strict();
19
105
  const RESOURCE_ID = z.string().trim().min(1).max(200);
20
106
  const HOLD_ID = z.string().trim().min(1).max(200);
21
107
  const EVENT_ID = z.string().trim().min(1).max(200);
@@ -58,9 +144,8 @@ const CURSOR = z.string().min(1).max(500);
58
144
  const NEXT_CURSOR = z.string().max(500).nullable();
59
145
  const VISIBILITY = z.enum(['shared', 'private']);
60
146
  /**
61
- * A retry identity, as the `Idempotency-Key` request header or this field (the same value, if both
62
- * are sent): the same key with the same request answers what the first did, and with another
63
- * request is 422 idempotency_conflict.
147
+ * A retry identity, as the `Idempotency-Key` header or this field (the same value if both): the
148
+ * same key and request answer what the first did; another request is 422 idempotency_conflict.
64
149
  */
65
150
  const IDEMPOTENCY_KEY = IDENTITY.describe('A retry identity, also accepted as the Idempotency-Key header: the same key with the same request answers what the first did; with another request, 422 idempotency_conflict.');
66
151
  /** Minutes after the end kept busy as well, for turnaround: at most 30 days. */
@@ -94,6 +179,106 @@ const BOOKING_RULES = z
94
179
  /** Default booking rules for the tenant or a pool: a resource's own rules but check-in. */
95
180
  const DEFAULT_BOOKING_RULES = BOOKING_RULES.omit({ check_in_minutes: true }).strict();
96
181
  const POOL = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/);
182
+ /** The order slotlock_book_pool tries a pool's members in (RFC 0003 G7). */
183
+ const POOL_ORDER = z.enum(SLOTLOCK_POOL_ORDERS);
184
+ const PROFILE = SLOTLOCK_RESOURCE_PROFILE_LIMITS;
185
+ /** What a resource is: a hint for filtering and display that changes no booking. */
186
+ const RESOURCE_KIND = z.enum(SLOTLOCK_RESOURCE_KINDS);
187
+ const RESOURCE_LABEL = z.string().regex(SLOTLOCK_RESOURCE_LABEL);
188
+ const RESOURCE_ATTRIBUTES = z.record(z.string().min(1).max(PROFILE.attributeKey), z.union([z.string().max(PROFILE.attributeValue), z.number(), z.boolean()]));
189
+ /**
190
+ * A resource's profile, each field optional (resource-profile.ts): a field left out is kept, `null`
191
+ * clears a name, description or capacity, and `[]` and `{}` clear labels and attributes. The store
192
+ * also refuses attributes over 4 KiB, or with more than 50 keys (400 invalid_resource_profile).
193
+ */
194
+ const PROFILE_INPUT = {
195
+ /** Shown to agents and people instead of the reference. */
196
+ name: z.string().min(1).max(PROFILE.name).nullable().optional(),
197
+ description: z.string().min(1).max(PROFILE.description).nullable().optional(),
198
+ kind: RESOURCE_KIND.optional(),
199
+ /** People or seats it holds: informational, never enforced as bookings at once. */
200
+ capacity: z.number().int().min(1).max(PROFILE.capacity).nullable().optional(),
201
+ /** Replaces its labels; sorted and without repeats when stored. */
202
+ labels: z.array(RESOURCE_LABEL).max(PROFILE.labels).optional(),
203
+ /** Replaces its attributes: text, numbers and true or false. */
204
+ attributes: RESOURCE_ATTRIBUTES.optional(),
205
+ };
206
+ /** A location's or a group's reference: as a pool's. */
207
+ const GROUPING_REF = z.string().regex(SLOTLOCK_GROUPING_REF);
208
+ /** An approval policy's reference (RFC 0004 A4). */
209
+ const APPROVAL_POLICY_REF = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/);
210
+ const APPROVAL_APPLIES_TO = z
211
+ .object({
212
+ resource_ids: z.array(z.string().uuid()).max(50).optional(),
213
+ pools: z.array(GROUPING_REF).max(50).optional(),
214
+ locations: z.array(GROUPING_REF).max(50).optional(),
215
+ groups: z.array(GROUPING_REF).max(50).optional(),
216
+ })
217
+ .strict();
218
+ const APPROVAL_CONDITIONS = z
219
+ .object({
220
+ longer_than_minutes: z.number().int().min(0).max(5_270_400).optional(),
221
+ outside_hours: z.boolean().optional(),
222
+ kinds: z.array(z.enum(SLOTLOCK_APPROVAL_KINDS)).min(1).max(4).optional(),
223
+ })
224
+ .strict();
225
+ const APPROVAL_POLICY = z
226
+ .object({
227
+ policy_ref: APPROVAL_POLICY_REF,
228
+ name: z.string().min(1).max(200),
229
+ applies_to: APPROVAL_APPLIES_TO,
230
+ conditions: APPROVAL_CONDITIONS,
231
+ outcome: z.enum(['require_approval', 'refuse']),
232
+ approval_ttl_minutes: z.number().int().min(1).max(SLOTLOCK_MAX_APPROVAL_MINUTES),
233
+ lead_minutes: z.number().int().min(0).max(SLOTLOCK_MAX_APPROVAL_MINUTES),
234
+ created_at: DATE_TIME,
235
+ updated_at: DATE_TIME,
236
+ })
237
+ .strict();
238
+ const APPROVAL_STATUS = z.enum(['pending', 'granted', 'declined', 'expired']);
239
+ /** A write waiting for a person, or decided. */
240
+ const APPROVAL = z
241
+ .object({
242
+ approval_id: z.string().uuid(),
243
+ policy_ref: APPROVAL_POLICY_REF,
244
+ status: APPROVAL_STATUS,
245
+ /** The agent operation it runs when granted, with its arguments as the agent sent them. */
246
+ operation: z.enum(['slotlock_create_event', 'slotlock_book_together', 'slotlock_update_event']),
247
+ request: z.record(z.string(), z.unknown()),
248
+ requested_by: z.string().max(200).nullable(),
249
+ resource_ids: z.array(z.string().uuid()).min(1).max(20),
250
+ starts_at: DATE_TIME,
251
+ ends_at: DATE_TIME,
252
+ expires_at: DATE_TIME,
253
+ decided_by: z.string().max(500).nullable(),
254
+ decided_at: DATE_TIME.nullable(),
255
+ reason: z.string().max(500).nullable(),
256
+ created_at: DATE_TIME,
257
+ })
258
+ .strict();
259
+ /** A place a tenant's resources are at (RFC 0003 G2). */
260
+ const LOCATION = z
261
+ .object({
262
+ ref: GROUPING_REF,
263
+ name: z.string().max(PROFILE.name),
264
+ /** The location it is in; `null` at the top. */
265
+ parent_ref: GROUPING_REF.nullable(),
266
+ /** What a resource created here without a time zone takes; `null` to take its parent's. */
267
+ timezone: TIMEZONE.nullable(),
268
+ /** Resources at it, not counting those under it. */
269
+ resources: z.number().int().min(0),
270
+ })
271
+ .strict();
272
+ /** A set of a tenant's resources (RFC 0003 G3). */
273
+ const GROUP = z
274
+ .object({
275
+ ref: GROUPING_REF,
276
+ name: z.string().max(PROFILE.name),
277
+ description: z.string().max(PROFILE.description).nullable(),
278
+ /** Resources in it. */
279
+ members: z.number().int().min(0),
280
+ })
281
+ .strict();
97
282
  const RESOURCE = z
98
283
  .object({
99
284
  id: RESOURCE_ID,
@@ -104,8 +289,7 @@ const RESOURCE = z
104
289
  /** Its own booking rules; `null` when it has none. */
105
290
  booking_rules: BOOKING_RULES.nullable(),
106
291
  /**
107
- * What a booking on it must satisfy: the tenant's default rules, overlaid by its pool's,
108
- * overlaid by its own; `null` when no rule applies.
292
+ * Tenant defaults, overlaid by its pool's, overlaid by its own; `null` when no rule applies.
109
293
  */
110
294
  effective_booking_rules: BOOKING_RULES.nullable(),
111
295
  /** `private` resources are never shown to agents; fixed when the resource is created. */
@@ -114,6 +298,23 @@ const RESOURCE = z
114
298
  archived_at: DATE_TIME.nullable(),
115
299
  /** The pool it is a member of (a vehicle class, a room type); `null` when none. */
116
300
  pool: POOL.nullable(),
301
+ /** Its name; `null` when it has none, and agents and people see the reference. */
302
+ name: z.string().max(PROFILE.name).nullable(),
303
+ description: z.string().max(PROFILE.description).nullable(),
304
+ /** What it is; `other` when not said. */
305
+ kind: RESOURCE_KIND,
306
+ /** People or seats it holds; `null` when not said. */
307
+ capacity: z.number().int().nullable(),
308
+ labels: z.array(RESOURCE_LABEL).max(PROFILE.labels),
309
+ attributes: RESOURCE_ATTRIBUTES,
310
+ /** The location it is at; `null` when not said. */
311
+ location: GROUPING_REF.nullable(),
312
+ /** The groups it is in, by reference. */
313
+ groups: z.array(GROUPING_REF).max(SLOTLOCK_MAX_GROUPS_PER_RESOURCE),
314
+ /** The composite it is a part of (a room of a hall), by id; `null` when it is no part. */
315
+ parent_id: z.string().uuid().nullable(),
316
+ /** Its place among its parent's parts, from 0; `null` when it is no part. */
317
+ part_index: z.number().int().min(0).nullable(),
117
318
  })
118
319
  .strict();
119
320
  const HISTORY_ENTRY = z
@@ -166,6 +367,16 @@ const WEBHOOK = z
166
367
  last_error: z.string().max(500).nullable(),
167
368
  disabled_at: DATE_TIME.nullable(),
168
369
  disabled_reason: z.enum(['gone', 'failing']).nullable(),
370
+ /** Only changes about these resources; `null` when it has no such filter. */
371
+ filter: z
372
+ .object({
373
+ resource_ids: z.array(z.string().uuid()).max(SLOTLOCK_WEBHOOK_FILTER_RESOURCES).optional(),
374
+ pools: z.array(GROUPING_REF).max(SLOTLOCK_WEBHOOK_FILTER_REFS).optional(),
375
+ locations: z.array(GROUPING_REF).max(SLOTLOCK_WEBHOOK_FILTER_REFS).optional(),
376
+ groups: z.array(GROUPING_REF).max(SLOTLOCK_WEBHOOK_FILTER_REFS).optional(),
377
+ })
378
+ .strict()
379
+ .nullable(),
169
380
  })
170
381
  .strict();
171
382
  const WAITLIST_ENTRY = z
@@ -208,11 +419,6 @@ const RESERVATION = z
208
419
  source: z.string().max(200).nullable(),
209
420
  })
210
421
  .strict();
211
- /**
212
- * Any of the tenant's events, whichever key wrote it: `id` is the store's id for it, which the
213
- * tenant-event routes take, `event_ref` the id its owner reads it by, and `owner_ref` who that is
214
- * (`internal` for the tenant's own code, `agent:<digest>` for an API key).
215
- */
216
422
  /** A JSCalendar Event (RFC 8984 §5.1); interop.ts's JSCalendarEvent names every property. */
217
423
  const JSCALENDAR_EVENT = z
218
424
  .object({
@@ -244,20 +450,175 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
244
450
  {
245
451
  name: 'slotlock_create_resource',
246
452
  title: 'Create or update a resource',
247
- description: 'Create the resource with this external reference, or update its time zone if the tenant already has it. Repeating the call changes nothing more. visibility is shared (the default: agents list and book it) or private (agents never see it), fixed at creation: naming another for an existing resource answers 409 resource_visibility_conflict.',
453
+ description: 'Create the resource with this external reference, or update its time zone, and the profile fields and location given, if the tenant already has it. Without a time zone, a new resource takes the nearest one its location, or a location above it, has, else UTC; a location the tenant does not have answers 404 location_not_found. Repeating the call changes nothing more. visibility is shared (the default: agents list and book it) or private (agents never see it), fixed at creation: naming another for an existing resource answers 409 resource_visibility_conflict. Its profile is optional: a name agents and people see instead of the reference, a description, its kind (person, room, desk, space, vehicle, machine, equipment, service or other: a hint, never a rule), its capacity (informational), up to 20 labels to find it by, and attributes you want back.',
248
454
  method: 'POST',
249
455
  path: '/v1/resources',
250
456
  input: z
251
457
  .object({
252
458
  external_ref: IDENTITY,
253
- timezone: TIMEZONE.default('UTC'),
459
+ /** UTC, or its location's, when not given. */
460
+ timezone: TIMEZONE.optional(),
254
461
  visibility: VISIBILITY.optional(),
462
+ ...PROFILE_INPUT,
463
+ /** The location it is at, by reference. */
464
+ location: GROUPING_REF.optional(),
255
465
  })
256
466
  .strict(),
257
467
  output: z.object({ resource: RESOURCE }).strict(),
258
468
  risk: { readOnly: false, destructive: false, idempotent: true },
259
469
  invoke: 'createResource',
260
- example: { external_ref: 'vehicle-42', timezone: 'Europe/London' },
470
+ example: {
471
+ external_ref: 'room-3',
472
+ timezone: 'Europe/London',
473
+ name: 'Room 3',
474
+ kind: 'room',
475
+ capacity: 10,
476
+ labels: ['video', 'whiteboard'],
477
+ },
478
+ },
479
+ {
480
+ name: 'slotlock_update_resource',
481
+ title: "Change a resource's profile",
482
+ description: "Change a resource's name, description, kind, capacity, labels, attributes or location: the fields given, keeping the rest. null clears a name, description, capacity or location; [] and {} clear labels and attributes. A location the tenant does not have answers 404 location_not_found. The feed's resource.updated names the fields that changed (profile_changed), never their values. 400 invalid_resource_profile outside the bounds, naming the field.",
483
+ method: 'PATCH',
484
+ path: '/v1/resources/{resource_id}',
485
+ input: z
486
+ .object({ resource_id: RESOURCE_ID, ...PROFILE_INPUT, location: GROUPING_REF.nullable().optional() })
487
+ .strict()
488
+ .refine((input) => Object.keys(input).some((key) => key !== 'resource_id'), 'Give at least one profile field to change'),
489
+ output: z.object({ resource: RESOURCE }).strict(),
490
+ risk: { readOnly: false, destructive: false, idempotent: true },
491
+ invoke: 'updateResource',
492
+ example: { resource_id: EXAMPLE_RESOURCE_ID, name: 'Board room', labels: ['video'] },
493
+ },
494
+ {
495
+ name: 'slotlock_list_locations',
496
+ title: 'List locations',
497
+ description: `The tenant's locations, each after the one it is in (depth first), with how many resources are at each. A tree of at most ${SLOTLOCK_MAX_LOCATION_DEPTH} levels and ${SLOTLOCK_MAX_LOCATIONS} locations: a region, a site, a building, a floor, a zone, or whatever yours are.`,
498
+ method: 'GET',
499
+ path: '/v1/locations',
500
+ input: z.object({}).strict(),
501
+ output: z.object({ locations: z.array(LOCATION).max(SLOTLOCK_MAX_LOCATIONS) }).strict(),
502
+ risk: { readOnly: true, destructive: false, idempotent: true },
503
+ invoke: 'listLocations',
504
+ example: {},
505
+ },
506
+ {
507
+ name: 'slotlock_create_location',
508
+ title: 'Create or update a location',
509
+ description: `Create the location with this reference, or change its name, and the parent and time zone given. 404 location_not_found for a parent the tenant does not have; 409 location_too_deep past ${SLOTLOCK_MAX_LOCATION_DEPTH} levels, location_cycle under itself, location_limit_reached past ${SLOTLOCK_MAX_LOCATIONS}.`,
510
+ method: 'POST',
511
+ path: '/v1/locations',
512
+ input: z
513
+ .object({
514
+ ref: GROUPING_REF,
515
+ name: z.string().min(1).max(PROFILE.name),
516
+ parent_ref: GROUPING_REF.nullable().optional(),
517
+ timezone: TIMEZONE.nullable().optional(),
518
+ })
519
+ .strict(),
520
+ output: z.object({ location: LOCATION }).strict(),
521
+ risk: { readOnly: false, destructive: false, idempotent: true },
522
+ invoke: 'createLocation',
523
+ example: { ref: 'leeds', name: 'Leeds clinic', timezone: 'Europe/London' },
524
+ },
525
+ {
526
+ name: 'slotlock_update_location',
527
+ title: 'Change a location',
528
+ description: "Change a location's name, parent or time zone: the fields given. Refused as creating one is.",
529
+ method: 'PATCH',
530
+ path: '/v1/locations/{location_ref}',
531
+ input: z
532
+ .object({
533
+ location_ref: GROUPING_REF,
534
+ name: z.string().min(1).max(PROFILE.name).optional(),
535
+ parent_ref: GROUPING_REF.nullable().optional(),
536
+ timezone: TIMEZONE.nullable().optional(),
537
+ })
538
+ .strict()
539
+ .refine((input) => Object.keys(input).some((key) => key !== 'location_ref'), 'Give at least one field to change'),
540
+ output: z.object({ location: LOCATION }).strict(),
541
+ risk: { readOnly: false, destructive: false, idempotent: true },
542
+ invoke: 'updateLocation',
543
+ example: { location_ref: 'leeds', name: 'Leeds city clinic' },
544
+ },
545
+ {
546
+ name: 'slotlock_delete_location',
547
+ title: 'Delete a location',
548
+ description: 'Delete a location. Refused with 409 location_in_use while a resource is at it or a location is in it: move them first. deleted is false when the tenant has no such location.',
549
+ method: 'DELETE',
550
+ path: '/v1/locations/{location_ref}',
551
+ input: z.object({ location_ref: GROUPING_REF }).strict(),
552
+ output: z.object({ deleted: z.boolean() }).strict(),
553
+ risk: { readOnly: false, destructive: true, idempotent: true },
554
+ invoke: 'deleteLocation',
555
+ example: { location_ref: 'leeds' },
556
+ },
557
+ {
558
+ name: 'slotlock_list_groups',
559
+ title: 'List groups',
560
+ description: `The tenant's groups, by reference, with how many resources are in each: any sets you want (a team, a department, a contract), at most ${SLOTLOCK_MAX_RESOURCE_GROUPS}. A group changes nothing about how its resources are booked; a pool does that.`,
561
+ method: 'GET',
562
+ path: '/v1/groups',
563
+ input: z.object({}).strict(),
564
+ output: z.object({ groups: z.array(GROUP).max(SLOTLOCK_MAX_RESOURCE_GROUPS) }).strict(),
565
+ risk: { readOnly: true, destructive: false, idempotent: true },
566
+ invoke: 'listGroups',
567
+ example: {},
568
+ },
569
+ {
570
+ name: 'slotlock_create_group',
571
+ title: 'Create or update a group',
572
+ description: `Create the group with this reference, or change its name, and its description when given. 409 group_limit_reached past ${SLOTLOCK_MAX_RESOURCE_GROUPS}.`,
573
+ method: 'POST',
574
+ path: '/v1/groups',
575
+ input: z
576
+ .object({
577
+ ref: GROUPING_REF,
578
+ name: z.string().min(1).max(PROFILE.name),
579
+ description: z.string().min(1).max(PROFILE.description).nullable().optional(),
580
+ })
581
+ .strict(),
582
+ output: z.object({ group: GROUP }).strict(),
583
+ risk: { readOnly: false, destructive: false, idempotent: true },
584
+ invoke: 'createGroup',
585
+ example: { ref: 'physio', name: 'Physiotherapy team' },
586
+ },
587
+ {
588
+ name: 'slotlock_delete_group',
589
+ title: 'Delete a group',
590
+ description: 'Delete a group; its resources stay as they are. deleted is false when the tenant has no such group.',
591
+ method: 'DELETE',
592
+ path: '/v1/groups/{group_ref}',
593
+ input: z.object({ group_ref: GROUPING_REF }).strict(),
594
+ output: z.object({ deleted: z.boolean() }).strict(),
595
+ risk: { readOnly: false, destructive: true, idempotent: true },
596
+ invoke: 'deleteGroup',
597
+ example: { group_ref: 'physio' },
598
+ },
599
+ {
600
+ name: 'slotlock_add_group_member',
601
+ title: 'Put a resource in a group',
602
+ description: `Put one of the tenant's resources in a group. added is false when it was in it already. 404 group_not_found or resource_not_found; 409 resource_group_limit_reached past ${SLOTLOCK_MAX_GROUPS_PER_RESOURCE} groups a resource.`,
603
+ method: 'PUT',
604
+ path: '/v1/groups/{group_ref}/members/{resource_id}',
605
+ input: z.object({ group_ref: GROUPING_REF, resource_id: RESOURCE_ID }).strict(),
606
+ output: z.object({ added: z.boolean() }).strict(),
607
+ risk: { readOnly: false, destructive: false, idempotent: true },
608
+ invoke: 'addGroupMember',
609
+ example: { group_ref: 'physio', resource_id: EXAMPLE_RESOURCE_ID },
610
+ },
611
+ {
612
+ name: 'slotlock_remove_group_member',
613
+ title: 'Take a resource out of a group',
614
+ description: 'Take a resource out of a group. removed is false when it was not in it.',
615
+ method: 'DELETE',
616
+ path: '/v1/groups/{group_ref}/members/{resource_id}',
617
+ input: z.object({ group_ref: GROUPING_REF, resource_id: RESOURCE_ID }).strict(),
618
+ output: z.object({ removed: z.boolean() }).strict(),
619
+ risk: { readOnly: false, destructive: true, idempotent: true },
620
+ invoke: 'removeGroupMember',
621
+ example: { group_ref: 'physio', resource_id: EXAMPLE_RESOURCE_ID },
261
622
  },
262
623
  {
263
624
  name: 'slotlock_get_resource',
@@ -390,20 +751,48 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
390
751
  invoke: 'setResourcePool',
391
752
  example: { resource_id: EXAMPLE_RESOURCE_ID, pool: 'van' },
392
753
  },
754
+ {
755
+ name: 'slotlock_set_resource_parent',
756
+ title: "Make a resource part of another",
757
+ description: "Make a resource a part of a composite (a room of a hall, a half of a court), or no part (`null`). A booking of the composite as a whole then conflicts with a booking of any of its parts, and two parts never conflict with each other; each part's free/busy shows the whole's bookings, and the whole's shows its parts'. One level deep, at most 50 parts; refused while either side has a booking or live hold still to come (`composite_busy`).",
758
+ method: 'PUT',
759
+ path: '/v1/resources/{resource_id}/parent',
760
+ input: z.object({ resource_id: RESOURCE_ID, parent_id: RESOURCE_ID.nullable() }).strict(),
761
+ output: z.object({ resource: RESOURCE }).strict(),
762
+ risk: { readOnly: false, destructive: false, idempotent: true },
763
+ invoke: 'setResourceParent',
764
+ example: { resource_id: EXAMPLE_RESOURCE_ID, parent_id: '0e7c1f9a-3b5d-4c2e-9f81-6a4b2d8c7e10' },
765
+ },
393
766
  {
394
767
  name: 'slotlock_list_pools',
395
768
  title: 'List pools',
396
- description: "The tenant's pools by name, each with its capacity: how many members it has that are not archived.",
769
+ description: "The tenant's pools by name, each with its capacity (how many members it has that are not archived) and the order slotlock_book_pool tries them in.",
397
770
  method: 'GET',
398
771
  path: '/v1/pools',
399
772
  input: z.object({}).strict(),
400
773
  output: z
401
- .object({ pools: z.array(z.object({ pool: POOL, capacity: z.number().int().min(1) }).strict()).max(1000) })
774
+ .object({
775
+ pools: z
776
+ .array(z.object({ pool: POOL, capacity: z.number().int().min(1), order: POOL_ORDER }).strict())
777
+ .max(1000),
778
+ })
402
779
  .strict(),
403
780
  risk: { readOnly: true, destructive: false, idempotent: true },
404
781
  invoke: 'listPools',
405
782
  example: {},
406
783
  },
784
+ {
785
+ name: 'slotlock_set_pool_order',
786
+ title: "Set a pool's order",
787
+ description: "The order slotlock_book_pool tries a pool's members in: stable (by id, the default), least_booked (fewest bookings made on each in the last 30 days first, to share them out) or round_robin (from the member after the one booked last). At most 1,000 pools have one: one more is 409 pool_order_limit_reached.",
788
+ method: 'PUT',
789
+ path: '/v1/pools/{pool}/order',
790
+ input: z.object({ pool: POOL, order: POOL_ORDER }).strict(),
791
+ output: z.object({ pool: POOL, order: POOL_ORDER }).strict(),
792
+ risk: { readOnly: false, destructive: false, idempotent: true },
793
+ invoke: 'setPoolOrder',
794
+ example: { pool: 'van', order: 'least_booked' },
795
+ },
407
796
  {
408
797
  name: 'slotlock_get_booking_rule_defaults',
409
798
  title: 'Get default booking rules',
@@ -511,7 +900,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
511
900
  .max(7 * 24 * 60 * 60),
512
901
  source: SOURCE.optional(),
513
902
  idempotency_key: IDEMPOTENCY_KEY.optional(),
514
- // Last: a generated client's 0.2.0 positional arguments keep their places.
903
+ // Last, so a generated 0.2.0 client's positional arguments keep their places.
515
904
  buffer_after_minutes: BUFFER_AFTER_MINUTES.optional(),
516
905
  })
517
906
  .strict()
@@ -670,7 +1059,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
670
1059
  {
671
1060
  name: 'slotlock_join_waitlist',
672
1061
  title: 'Join the waitlist',
673
- description: "Wait for a window on one resource (resource_id) or on any member of a pool (pool) that is taken now. When it comes free (a booking cancelled, a hold released or expired, a no-show), the entries waiting for it are offered in the order they joined: the first gets a hold of it for holder_ref, for offer_ttl_seconds (default 900), and the feed says so (waitlist.offered, with hold_id): confirm the hold to book it. An entry whose window starts first expires (waitlist.expired). A holder may wait for 20 windows at once (409 waitlist_limit_reached); a window is at most 31 days.",
1062
+ description: "Wait for a window on one resource (resource_id) or on any member of a pool (pool) that is taken now. When it comes free (a booking cancelled, a hold released or expired, a no-show), the entries waiting for it are offered in the order they joined: the first gets a hold of it for holder_ref, for offer_ttl_seconds (default 900), and the feed says so (waitlist.offered, with hold_id): confirm the hold to book it. An entry whose window starts first expires (waitlist.expired). A holder may wait for 20 windows at once (409 waitlist_limit_reached); a window is at most 31 days. With an idempotency key (the Idempotency-Key header or idempotency_key), a retry answers the entry the first request made (replayed: true) for as long as it is kept; the same key with another request is 422 idempotency_conflict.",
674
1063
  method: 'POST',
675
1064
  path: '/v1/waitlist',
676
1065
  input: z
@@ -681,13 +1070,14 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
681
1070
  end: DATE_TIME,
682
1071
  holder_ref: z.string().trim().min(1).max(500),
683
1072
  offer_ttl_seconds: z.number().int().min(60).max(86_400).optional(),
1073
+ idempotency_key: IDEMPOTENCY_KEY.optional(),
684
1074
  })
685
1075
  .strict()
686
1076
  .refine((input) => (input.resource_id === undefined) !== (input.pool === undefined), {
687
1077
  message: 'Name resource_id or pool, not both',
688
1078
  }),
689
- output: z.object({ entry: WAITLIST_ENTRY }).strict(),
690
- risk: { readOnly: false, destructive: false, idempotent: false },
1079
+ output: z.object({ entry: WAITLIST_ENTRY, replayed: z.boolean() }).strict(),
1080
+ risk: { readOnly: false, destructive: false, idempotent: true },
691
1081
  invoke: 'joinWaitlist',
692
1082
  example: {
693
1083
  pool: 'van',
@@ -768,8 +1158,8 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
768
1158
  .object({
769
1159
  source: SOURCE,
770
1160
  /**
771
- * How the change feed names the source (coverage.recorded, .deleted, .stale): random,
772
- * made when it is first recorded for the resource, kept until it is deleted.
1161
+ * How the change feed names the source: random, made when first recorded for the
1162
+ * resource, kept until deleted.
773
1163
  */
774
1164
  source_id: z.string().regex(/^src_[0-9a-f]{32}$/),
775
1165
  start: DATE_TIME,
@@ -870,8 +1260,8 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
870
1260
  event_id: EVENT_ID,
871
1261
  expected_revision: REVISION,
872
1262
  expected_resource_id: RESOURCE_ID.optional(),
873
- // Required, so a client always sends it; the Idempotency-Key header may stand in for
874
- // it, since the server reads the header into it before the schema checks the request.
1263
+ // Required, so clients always send it; the Idempotency-Key header may stand in, since the
1264
+ // server copies it in before validation.
875
1265
  idempotency_key: IDEMPOTENCY_KEY,
876
1266
  })
877
1267
  .strict(),
@@ -908,7 +1298,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
908
1298
  {
909
1299
  name: 'slotlock_erase_tenant',
910
1300
  title: 'Erase the tenant',
911
- description: "Delete everything the key's tenant has, at once and for good: resources, holds, bookings, events, coverage and every API key, this one included. confirm_tenant_ref must be the tenant's reference (slotlock_get_tenant), or it answers 400 invalid_confirmation. Off unless the server allows it (403 tenant_erasure_disabled; slotlock serve does not: its operator runs slotlock tenant erase). A suspended tenant cannot erase itself (403 tenant_suspended). Erasing again deletes nothing.",
1301
+ description: "Delete everything the key's tenant has, at once and for good: resources, holds, bookings, events, coverage and every API key, this one included. confirm_tenant_ref must be the tenant's reference (slotlock_get_tenant), or it answers 400 invalid_confirmation. Off unless the server allows it (403 tenant_erasure_disabled; slotlock serve does not: its operator runs slotlock tenant erase). A suspended tenant cannot erase itself (403 tenant_suspended), nor can one whose hosted plan is still paid for until it is cancelled (409 billing_active). Erasing again deletes nothing.",
912
1302
  method: 'POST',
913
1303
  path: '/v1/tenant/erase',
914
1304
  input: z.object({ confirm_tenant_ref: IDENTITY }).strict(),
@@ -940,6 +1330,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
940
1330
  stale_sources: z.number().int().min(0),
941
1331
  no_shows: z.number().int().min(0),
942
1332
  waitlist_offers: z.number().int().min(0),
1333
+ approvals_expired: z.number().int().min(0),
943
1334
  capped: z.boolean(),
944
1335
  })
945
1336
  .strict(),
@@ -964,17 +1355,22 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
964
1355
  {
965
1356
  name: 'slotlock_create_webhook',
966
1357
  title: 'Add a webhook',
967
- description: 'Send the changes slotlock_list_changes lists from now on to an https URL, each as a CloudEvent signed as Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature), in order, until a 2xx. A failure is retried with backoff; after 24 hours of failures, or a 410, the webhook is disabled and the feed records webhook.disabled. The secret is returned here only. At most 10 per tenant.',
1358
+ description: `Send the changes slotlock_list_changes lists from now on to an https URL, each as a CloudEvent signed as Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature), in order, until a 2xx. A failure is retried with backoff; after 24 hours of failures, or a 410, the webhook is disabled and the feed records webhook.disabled. The secret is returned here only. At most ${SLOTLOCK_MAX_WEBHOOK_ENDPOINTS} per tenant, or fewer on a plan (409 webhook_limit_reached). With resource_ids, pools, locations (each with every location in it) or groups, only changes about a resource that matches one of them, as it is when the change is delivered: a change about no resource (a key, a webhook, a usage notice) is not sent. At most ${SLOTLOCK_WEBHOOK_FILTER_RESOURCES} resource ids and ${SLOTLOCK_WEBHOOK_FILTER_REFS} of each other kind (400 invalid_webhook_filter). With an idempotency key (the Idempotency-Key header or idempotency_key), a retry answers the webhook and secret the first request made (replayed: true) for as long as the webhook is kept; the same key with another request is 422 idempotency_conflict.`,
968
1359
  method: 'POST',
969
1360
  path: '/v1/webhooks',
970
1361
  input: z
971
1362
  .object({
972
1363
  url: z.string().url().max(2048),
973
1364
  event_types: z.array(z.enum(SLOTLOCK_CHANGE_TYPES)).min(1).max(SLOTLOCK_CHANGE_TYPES.length).optional(),
1365
+ resource_ids: z.array(z.string().uuid()).min(1).max(SLOTLOCK_WEBHOOK_FILTER_RESOURCES).optional(),
1366
+ pools: z.array(GROUPING_REF).min(1).max(SLOTLOCK_WEBHOOK_FILTER_REFS).optional(),
1367
+ locations: z.array(GROUPING_REF).min(1).max(SLOTLOCK_WEBHOOK_FILTER_REFS).optional(),
1368
+ groups: z.array(GROUPING_REF).min(1).max(SLOTLOCK_WEBHOOK_FILTER_REFS).optional(),
1369
+ idempotency_key: IDEMPOTENCY_KEY.optional(),
974
1370
  })
975
1371
  .strict(),
976
- output: z.object({ webhook: WEBHOOK, secret: z.string().regex(/^whsec_/) }).strict(),
977
- risk: { readOnly: false, destructive: false, idempotent: false },
1372
+ output: z.object({ webhook: WEBHOOK, secret: z.string().regex(/^whsec_/), replayed: z.boolean() }).strict(),
1373
+ risk: { readOnly: false, destructive: false, idempotent: true },
978
1374
  invoke: 'createWebhook',
979
1375
  example: { url: 'https://example.com/slotlock-webhook', event_types: ['reservation.created'] },
980
1376
  },
@@ -985,7 +1381,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
985
1381
  method: 'GET',
986
1382
  path: '/v1/webhooks',
987
1383
  input: z.object({}).strict(),
988
- output: z.object({ webhooks: z.array(WEBHOOK).max(10) }).strict(),
1384
+ output: z.object({ webhooks: z.array(WEBHOOK).max(SLOTLOCK_MAX_WEBHOOK_ENDPOINTS) }).strict(),
989
1385
  risk: { readOnly: true, destructive: false, idempotent: true },
990
1386
  invoke: 'listWebhooks',
991
1387
  example: {},
@@ -1014,6 +1410,413 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
1014
1410
  invoke: 'enableWebhook',
1015
1411
  example: { webhook_id: '9b2f4c1e-6d3a-4e8b-a1c7-5f0e2d4b6a8c' },
1016
1412
  },
1413
+ {
1414
+ name: 'slotlock_get_usage',
1415
+ title: 'Read usage',
1416
+ description: "How much the tenant used the service from one UTC day to another (inclusive, at most 400 days apart; by default the first of this month to today): operations its keys and the dashboard assistant ran (each call that reached its operation, whatever it returned), bookings (bookings, events and confirmed holds) and holds it made, webhook deliveries its endpoints accepted, messages to the dashboard assistant, and the resources it held (each day's count, archived ones included; in totals, the last day's, not a sum). Totals, then by day, by API key (key_id null for none) and by surface (mcp, a2a, rest, application, dashboard-assistant, webhooks); resources are by day only. Counts only, never what was asked. Empty unless the server meters usage (SLOTLOCK_USAGE=on); operations, deliveries and messages appear within 15 seconds; bookings, holds and resources within the hour. 400 invalid_usage_range when to is before from, the range is longer than 400 days, or to is after today.",
1417
+ method: 'GET',
1418
+ path: '/v1/usage',
1419
+ input: z.object({ from: USAGE_DAY.optional(), to: USAGE_DAY.optional() }).strict(),
1420
+ output: z
1421
+ .object({
1422
+ from: USAGE_DAY,
1423
+ to: USAGE_DAY,
1424
+ totals: z.object(USAGE_TOTALS).strict(),
1425
+ days: z.array(z.object({ day: USAGE_DAY, ...USAGE_TOTALS }).strict()).max(401),
1426
+ keys: z
1427
+ .array(z.object({ key_id: z.string().uuid().nullable(), ...USAGE_TOTALS }).strict())
1428
+ .max(1001),
1429
+ surfaces: z.array(z.object({ origin: z.string().min(1).max(40), ...USAGE_TOTALS }).strict()).max(20),
1430
+ })
1431
+ .strict(),
1432
+ risk: { readOnly: true, destructive: false, idempotent: true },
1433
+ invoke: 'getUsage',
1434
+ example: { from: '2026-11-01', to: '2026-11-30' },
1435
+ },
1436
+ // Approval policies (RFC 0004 A4): writes a person approves before they are made.
1437
+ {
1438
+ name: 'slotlock_list_approval_policies',
1439
+ title: 'List approval policies',
1440
+ description: "The tenant's approval policies, by reference: which agent bookings wait for a person (require_approval) or are refused (refuse).",
1441
+ method: 'GET',
1442
+ path: '/v1/approval-policies',
1443
+ input: z.object({}).strict(),
1444
+ output: z.object({ policies: z.array(APPROVAL_POLICY).max(SLOTLOCK_MAX_APPROVAL_POLICIES) }).strict(),
1445
+ risk: { readOnly: true, destructive: false, idempotent: true },
1446
+ invoke: 'listApprovalPolicies',
1447
+ example: {},
1448
+ },
1449
+ {
1450
+ name: 'slotlock_put_approval_policy',
1451
+ title: 'Create or replace an approval policy',
1452
+ description: "Which agent bookings (slotlock_create_event, slotlock_confirm_slot, slotlock_book_pool, slotlock_book_together, and moves by slotlock_update_event or slotlock_reschedule) a person approves first, or are refused. applies_to names resources, pools, locations (with the locations in them) and groups; empty, every resource. conditions narrow it: longer_than_minutes, outside_hours (outside the resource's bookable hours), kinds (single, pool, bundle, move); a booking meets the policy when it meets every condition given. outcome require_approval holds the exact time the booking would take, under the approval, and answers the agent 409 approval_pending; refuse answers 403 approval_policy_refused. A person has approval_ttl_minutes to decide (default 1,440; at most 43,200, and the server's ceiling), and must decide lead_minutes before the booking starts: the wait is the earlier of the two. Where several apply, refuse wins, then the one that must be decided soonest. At most 50 policies: one more is 409 approval_policy_limit_reached.",
1453
+ method: 'PUT',
1454
+ path: '/v1/approval-policies/{policy_ref}',
1455
+ input: z
1456
+ .object({
1457
+ policy_ref: APPROVAL_POLICY_REF,
1458
+ name: z.string().trim().min(1).max(200),
1459
+ applies_to: APPROVAL_APPLIES_TO.optional(),
1460
+ conditions: APPROVAL_CONDITIONS.optional(),
1461
+ outcome: z.enum(['require_approval', 'refuse']),
1462
+ approval_ttl_minutes: z.number().int().min(1).max(SLOTLOCK_MAX_APPROVAL_MINUTES).optional(),
1463
+ lead_minutes: z.number().int().min(0).max(SLOTLOCK_MAX_APPROVAL_MINUTES).optional(),
1464
+ })
1465
+ .strict(),
1466
+ output: z.object({ policy: APPROVAL_POLICY }).strict(),
1467
+ risk: { readOnly: false, destructive: false, idempotent: true },
1468
+ invoke: 'putApprovalPolicy',
1469
+ example: {
1470
+ policy_ref: 'long-van-hires',
1471
+ name: 'Van hires over a day',
1472
+ applies_to: { pools: ['van'] },
1473
+ conditions: { longer_than_minutes: 1440 },
1474
+ outcome: 'require_approval',
1475
+ approval_ttl_minutes: 240,
1476
+ lead_minutes: 60,
1477
+ },
1478
+ },
1479
+ {
1480
+ name: 'slotlock_delete_approval_policy',
1481
+ title: 'Delete an approval policy',
1482
+ description: 'Stop applying a policy to new bookings. Approvals it already asked for stay as they are, to be decided.',
1483
+ method: 'DELETE',
1484
+ path: '/v1/approval-policies/{policy_ref}',
1485
+ input: z.object({ policy_ref: APPROVAL_POLICY_REF }).strict(),
1486
+ output: z.object({ deleted: z.boolean() }).strict(),
1487
+ risk: { readOnly: false, destructive: true, idempotent: true },
1488
+ invoke: 'deleteApprovalPolicy',
1489
+ example: { policy_ref: 'long-van-hires' },
1490
+ },
1491
+ {
1492
+ name: 'slotlock_list_approvals',
1493
+ title: 'List approvals',
1494
+ description: "The tenant's approvals, newest first: bookings an agent asked for that a policy holds for a person (pending, holding their time until expires_at), and those decided (granted, declined, expired). Filter by status.",
1495
+ method: 'GET',
1496
+ path: '/v1/approvals',
1497
+ input: z
1498
+ .object({
1499
+ status: APPROVAL_STATUS.optional(),
1500
+ limit: z.coerce.number().int().min(1).max(200).default(50),
1501
+ })
1502
+ .strict(),
1503
+ output: z.object({ approvals: z.array(APPROVAL).max(200) }).strict(),
1504
+ risk: { readOnly: true, destructive: false, idempotent: true },
1505
+ invoke: 'listApprovals',
1506
+ example: { status: 'pending', limit: 50 },
1507
+ },
1508
+ {
1509
+ name: 'slotlock_get_approval',
1510
+ title: 'Get an approval',
1511
+ description: 'One approval by id, with the booking it would make. 404 approval_not_found.',
1512
+ method: 'GET',
1513
+ path: '/v1/approvals/{approval_id}',
1514
+ input: z.object({ approval_id: z.string().uuid() }).strict(),
1515
+ output: z.object({ approval: APPROVAL }).strict(),
1516
+ risk: { readOnly: true, destructive: false, idempotent: true },
1517
+ invoke: 'getApproval',
1518
+ example: { approval_id: '3f6b2a1c-8d4e-4b7a-9c2f-1e5d7a9b3c4d' },
1519
+ },
1520
+ {
1521
+ name: 'slotlock_decide_approval',
1522
+ title: 'Grant or decline an approval',
1523
+ description: "Decide a pending approval. grant makes the booking as the agent asked for it, as that agent, in one transaction with letting the approval's hold go, and answers it under booking; the agent's retry with its idempotency key then answers the same booking. A write refused now (a booking rule changed, say) is refused with its own code and the approval stays pending. decline frees the time; the agent's retry answers 403 approval_declined with the reason. 404 approval_not_found; 409 approval_not_pending when already decided; 409 approval_expired after its wait.",
1524
+ method: 'POST',
1525
+ path: '/v1/approvals/{approval_id}/decision',
1526
+ input: z
1527
+ .object({
1528
+ approval_id: z.string().uuid(),
1529
+ decision: z.enum(['grant', 'decline']),
1530
+ reason: z.string().trim().min(1).max(500).optional(),
1531
+ })
1532
+ .strict(),
1533
+ output: z.object({ approval: APPROVAL, booking: z.record(z.string(), z.unknown()).nullable() }).strict(),
1534
+ risk: { readOnly: false, destructive: false, idempotent: false },
1535
+ invoke: 'decideApproval',
1536
+ example: { approval_id: '3f6b2a1c-8d4e-4b7a-9c2f-1e5d7a9b3c4d', decision: 'grant' },
1537
+ },
1538
+ // The sandbox (RFC 0004 A6): a test environment's only (`sandbox_test_only` elsewhere).
1539
+ {
1540
+ name: 'slotlock_sandbox_seed',
1541
+ title: 'Seed the test environment',
1542
+ description: "Fill an empty test environment (a key starting slk_test_) with a scenario to try an agent against: car-rental (cars and vans in pools at two branches, 30-minute turnaround), clinic (practitioners and treatment rooms, half-hour slots on weekdays) or desks (desks on two floors, and a hall that splits in two). Locations, groups, resources with profiles, hours and booking rules, and three bookings tomorrow. 409 sandbox_not_empty when it has resources: reset it first. 403 sandbox_test_only for a live key.",
1543
+ method: 'POST',
1544
+ path: '/v1/sandbox/seed',
1545
+ input: z.object({ scenario: z.enum(['car-rental', 'clinic', 'desks']) }).strict(),
1546
+ output: z
1547
+ .object({
1548
+ scenario: z.enum(['car-rental', 'clinic', 'desks']),
1549
+ resources: z.number().int().min(0),
1550
+ locations: z.number().int().min(0),
1551
+ groups: z.number().int().min(0),
1552
+ pools: z.number().int().min(0),
1553
+ bookings: z.number().int().min(0),
1554
+ })
1555
+ .strict(),
1556
+ risk: { readOnly: false, destructive: false, idempotent: false },
1557
+ invoke: 'sandboxSeed',
1558
+ example: { scenario: 'car-rental' },
1559
+ },
1560
+ {
1561
+ name: 'slotlock_sandbox_reset',
1562
+ title: 'Empty the test environment',
1563
+ description: "Empty the test environment as if new: its resources, locations, groups, bookings, events, holds, waitlist, approval policies and feed. Its API keys, webhooks, plan and usage stay. Rows removed, by table. 403 sandbox_test_only for a live key.",
1564
+ method: 'POST',
1565
+ path: '/v1/sandbox/reset',
1566
+ input: z.object({}).strict(),
1567
+ output: z.object({ removed: z.record(z.string(), z.number().int().min(0)) }).strict(),
1568
+ risk: { readOnly: false, destructive: true, idempotent: true },
1569
+ invoke: 'sandboxReset',
1570
+ example: {},
1571
+ },
1572
+ {
1573
+ name: 'slotlock_sandbox_fast_forward',
1574
+ title: "Move the test environment's waiting forward",
1575
+ description: "Make what would lapse in the test environment within minutes (1-525,600) lapse now: holds (a waitlist offer's and an approval's among them) and pending approvals; then a maintenance pass reaps them as it would have, releasing holds, passing offers on and expiring approvals, on the feed as usual. Bookings and events keep their times; no-shows and series follow the real clock. 403 sandbox_test_only for a live key.",
1576
+ method: 'POST',
1577
+ path: '/v1/sandbox/fast-forward',
1578
+ input: z.object({ minutes: z.number().int().min(1).max(525_600) }).strict(),
1579
+ output: z
1580
+ .object({
1581
+ minutes: z.number().int(),
1582
+ holds: z.number().int().min(0),
1583
+ approvals: z.number().int().min(0),
1584
+ maintenance: z.record(z.string(), z.unknown()),
1585
+ })
1586
+ .strict(),
1587
+ risk: { readOnly: false, destructive: false, idempotent: false },
1588
+ invoke: 'sandboxFastForward',
1589
+ example: { minutes: 60 },
1590
+ },
1591
+ {
1592
+ name: 'slotlock_sandbox_race',
1593
+ title: 'Race agents for one slot in the test environment',
1594
+ description: "Race agents (2-200, default 100), each an agent of its own, for one time on one resource of the test environment, through the same path MCP, A2A and REST use: how many booked (one), how many were refused and why (overlap, with alternatives), any server errors, and how long the answers took. 403 sandbox_test_only for a live key.",
1595
+ method: 'POST',
1596
+ path: '/v1/sandbox/race',
1597
+ input: z
1598
+ .object({
1599
+ resource_id: RESOURCE_ID,
1600
+ starts_at: DATE_TIME,
1601
+ ends_at: DATE_TIME,
1602
+ agents: z.number().int().min(2).max(200).optional(),
1603
+ })
1604
+ .strict()
1605
+ .refine((value) => Date.parse(value.ends_at) > Date.parse(value.starts_at)),
1606
+ output: z
1607
+ .object({
1608
+ agents: z.number().int(),
1609
+ booked: z.number().int().min(0),
1610
+ refused: z.record(z.string(), z.number().int().min(0)),
1611
+ with_alternatives: z.number().int().min(0),
1612
+ server_errors: z.number().int().min(0),
1613
+ p50_ms: z.number().min(0),
1614
+ p99_ms: z.number().min(0),
1615
+ max_ms: z.number().min(0),
1616
+ })
1617
+ .strict(),
1618
+ risk: { readOnly: false, destructive: false, idempotent: false },
1619
+ invoke: 'sandboxRace',
1620
+ example: { resource_id: '00000000-0000-4000-8000-000000000001', starts_at: '2027-03-02T09:00:00Z', ends_at: '2027-03-02T10:00:00Z', agents: 100 },
1621
+ },
1622
+ // The partner API (RFC 0003 P3): a partner key's, for that partner's own accounts only.
1623
+ {
1624
+ name: 'slotlock_partner_put_account',
1625
+ title: 'Create or update an account',
1626
+ description: "Create the partner's account with this id (your own id for your user, 1-128 letters, digits, dots, dashes or underscores), or put it on another plan: one of your contract's, whose limits and monthly quotas the account then has. Its tenant is partner:<partner>:<account>, an ordinary Slotlock tenant of its own. Repeating the call changes nothing more. 400 invalid_partner_plan for a plan your contract does not have.",
1627
+ method: 'PUT',
1628
+ path: '/v1/partner/accounts/{account}',
1629
+ input: z.object({ account: PARTNER_ACCOUNT, plan: PARTNER_PLAN_NAME }).strict(),
1630
+ output: z.object({ account: PARTNER_ACCOUNT_OUTPUT, created: z.boolean() }).strict(),
1631
+ risk: { readOnly: false, destructive: false, idempotent: true },
1632
+ invoke: 'partnerPutAccount',
1633
+ scope: 'partner',
1634
+ example: { account: 'user-4821', plan: 'partner_pro' },
1635
+ },
1636
+ {
1637
+ name: 'slotlock_partner_list_accounts',
1638
+ title: 'List accounts',
1639
+ description: "The partner's accounts, by id: plan, whether suspended, and when made and last changed.",
1640
+ method: 'GET',
1641
+ path: '/v1/partner/accounts',
1642
+ input: z.object({}).strict(),
1643
+ output: z.object({ accounts: z.array(PARTNER_ACCOUNT_OUTPUT).max(10000) }).strict(),
1644
+ risk: { readOnly: true, destructive: false, idempotent: true },
1645
+ invoke: 'partnerListAccounts',
1646
+ scope: 'partner',
1647
+ example: {},
1648
+ },
1649
+ {
1650
+ name: 'slotlock_partner_get_account',
1651
+ title: 'Read an account',
1652
+ description: 'One of the partner\'s accounts, with how many resources (archived ones included) and active API keys it holds. 404 account_not_found for an account the partner does not have.',
1653
+ method: 'GET',
1654
+ path: '/v1/partner/accounts/{account}',
1655
+ input: z.object({ account: PARTNER_ACCOUNT }).strict(),
1656
+ output: z.object({ account: PARTNER_ACCOUNT_OUTPUT }).strict(),
1657
+ risk: { readOnly: true, destructive: false, idempotent: true },
1658
+ invoke: 'partnerGetAccount',
1659
+ scope: 'partner',
1660
+ example: { account: 'user-4821' },
1661
+ },
1662
+ {
1663
+ name: 'slotlock_partner_list_keys',
1664
+ title: "List an account's keys",
1665
+ description: "The account's API keys, revoked and expired ones included, newest first; never the keys themselves.",
1666
+ method: 'GET',
1667
+ path: '/v1/partner/accounts/{account}/keys',
1668
+ input: z.object({ account: PARTNER_ACCOUNT, environment: PARTNER_ENVIRONMENT }).strict(),
1669
+ output: z.object({ keys: z.array(PARTNER_KEY_OUTPUT).max(1000) }).strict(),
1670
+ risk: { readOnly: true, destructive: false, idempotent: true },
1671
+ invoke: 'partnerListKeys',
1672
+ scope: 'partner',
1673
+ example: { account: 'user-4821' },
1674
+ },
1675
+ {
1676
+ name: 'slotlock_partner_create_key',
1677
+ title: 'Create a key for an account',
1678
+ description: "An API key for the account, shown once in key and never again; only its SHA-256 is kept. Give it to your user's calls: it acts in the account's tenant, over MCP, A2A and REST, as any key. environment: test makes it in the account's test environment instead (a tenant of its own, on the same plan, never billed; its keys start slk_test_). scopes: some of the ones your contract allows (read and write unless it says), by default all of them; expires_in_days: 1-3650, or none. limits hold the key's agent below the server's limits: max_holds live at once, max_held_hours they hold together, requests_per_minute, and requires_approval to make every booking it asks for wait for a person (the account's Approvals). 403 scope_not_in_contract for another scope; 409 api_key_limit_reached past the account plan's keys.",
1679
+ method: 'POST',
1680
+ path: '/v1/partner/accounts/{account}/keys',
1681
+ input: z
1682
+ .object({
1683
+ account: PARTNER_ACCOUNT,
1684
+ name: z.string().trim().min(1).max(100),
1685
+ scopes: z.array(z.enum(['read', 'write', 'admin'])).min(1).max(3).optional(),
1686
+ expires_in_days: z.number().int().min(1).max(3650).optional(),
1687
+ environment: PARTNER_ENVIRONMENT,
1688
+ limits: KEY_LIMITS.optional(),
1689
+ })
1690
+ .strict(),
1691
+ output: z.object({ key: ACCOUNT_KEY, api_key: PARTNER_KEY_OUTPUT }).strict(),
1692
+ risk: { readOnly: false, destructive: false, idempotent: false },
1693
+ invoke: 'partnerCreateKey',
1694
+ scope: 'partner',
1695
+ example: { account: 'user-4821', name: 'Booking agent' },
1696
+ },
1697
+ {
1698
+ name: 'slotlock_partner_rotate_key',
1699
+ title: "Rotate an account's key",
1700
+ description: 'A new secret for one of the account\'s active keys, shown once; the old one stops at once. The key keeps its id and what it booked. 404 key_not_found otherwise.',
1701
+ method: 'POST',
1702
+ path: '/v1/partner/accounts/{account}/keys/{key_id}/rotate',
1703
+ input: z.object({ account: PARTNER_ACCOUNT, key_id: z.string().uuid(), environment: PARTNER_ENVIRONMENT }).strict(),
1704
+ output: z.object({ key: ACCOUNT_KEY, api_key: PARTNER_KEY_OUTPUT }).strict(),
1705
+ risk: { readOnly: false, destructive: false, idempotent: false },
1706
+ invoke: 'partnerRotateKey',
1707
+ scope: 'partner',
1708
+ example: { account: 'user-4821', key_id: '7d1e4f8a-2b3c-4d5e-8f90-123456789abc' },
1709
+ },
1710
+ {
1711
+ name: 'slotlock_partner_revoke_key',
1712
+ title: "Revoke an account's key",
1713
+ description: "Revoke one of the account's keys at once; revoking it again changes nothing. 404 key_not_found otherwise.",
1714
+ method: 'DELETE',
1715
+ path: '/v1/partner/accounts/{account}/keys/{key_id}',
1716
+ input: z.object({ account: PARTNER_ACCOUNT, key_id: z.string().uuid(), environment: PARTNER_ENVIRONMENT }).strict(),
1717
+ output: z.object({ api_key: PARTNER_KEY_OUTPUT }).strict(),
1718
+ risk: { readOnly: false, destructive: true, idempotent: true },
1719
+ invoke: 'partnerRevokeKey',
1720
+ scope: 'partner',
1721
+ example: { account: 'user-4821', key_id: '7d1e4f8a-2b3c-4d5e-8f90-123456789abc' },
1722
+ },
1723
+ {
1724
+ name: 'slotlock_partner_suspend_account',
1725
+ title: 'Suspend an account',
1726
+ description: 'Suspend the account, as slotlock tenant suspend: it may read, and writes nothing; its keys get no new secret. suspended says whether it was not suspended already.',
1727
+ method: 'POST',
1728
+ path: '/v1/partner/accounts/{account}/suspend',
1729
+ input: z.object({ account: PARTNER_ACCOUNT, reason: z.string().trim().min(1).max(200).optional() }).strict(),
1730
+ output: z.object({ suspended: z.boolean() }).strict(),
1731
+ risk: { readOnly: false, destructive: false, idempotent: true },
1732
+ invoke: 'partnerSuspendAccount',
1733
+ scope: 'partner',
1734
+ example: { account: 'user-4821', reason: 'unpaid' },
1735
+ },
1736
+ {
1737
+ name: 'slotlock_partner_unsuspend_account',
1738
+ title: 'Lift a suspension',
1739
+ description: 'Lift a suspension the partner made. One Pylota made stays: unsuspended is false then, as when the account was not suspended.',
1740
+ method: 'POST',
1741
+ path: '/v1/partner/accounts/{account}/unsuspend',
1742
+ input: z.object({ account: PARTNER_ACCOUNT }).strict(),
1743
+ output: z.object({ unsuspended: z.boolean() }).strict(),
1744
+ risk: { readOnly: false, destructive: false, idempotent: true },
1745
+ invoke: 'partnerUnsuspendAccount',
1746
+ scope: 'partner',
1747
+ example: { account: 'user-4821' },
1748
+ },
1749
+ {
1750
+ name: 'slotlock_partner_erase_account',
1751
+ title: 'Erase an account',
1752
+ description: "Erase the account's tenant, every row it owns, as /v1/tenant/erase, and the partner's record of it. confirm repeats the account. Cannot be undone.",
1753
+ method: 'POST',
1754
+ path: '/v1/partner/accounts/{account}/erase',
1755
+ input: z.object({ account: PARTNER_ACCOUNT, confirm: PARTNER_ACCOUNT }).strict(),
1756
+ output: z.object({ erased: z.boolean(), rows: z.number().int().min(0) }).strict(),
1757
+ risk: { readOnly: false, destructive: true, idempotent: true },
1758
+ invoke: 'partnerEraseAccount',
1759
+ scope: 'partner',
1760
+ example: { account: 'user-4821', confirm: 'user-4821' },
1761
+ },
1762
+ {
1763
+ name: 'slotlock_partner_get_usage',
1764
+ title: "Read the accounts' usage",
1765
+ description: "The partner's accounts' usage, the counts Pylota invoices from, by account, UTC day and metric (operations, bookings, holds, webhook_deliveries, resources: each day's count), from one UTC day to another (inclusive, at most 400 days apart; by default this month); one account, or all. 400 invalid_usage_range otherwise.",
1766
+ method: 'GET',
1767
+ path: '/v1/partner/usage',
1768
+ input: z.object({ from: USAGE_DAY.optional(), to: USAGE_DAY.optional(), account: PARTNER_ACCOUNT.optional() }).strict(),
1769
+ output: z
1770
+ .object({
1771
+ from: USAGE_DAY,
1772
+ to: USAGE_DAY,
1773
+ usage: z
1774
+ .array(z.object({ account: PARTNER_ACCOUNT, day: USAGE_DAY, metric: z.string().min(1).max(40), count: USAGE_COUNT }).strict())
1775
+ .max(500000),
1776
+ })
1777
+ .strict(),
1778
+ risk: { readOnly: true, destructive: false, idempotent: true },
1779
+ invoke: 'partnerGetUsage',
1780
+ scope: 'partner',
1781
+ example: { from: '2026-11-01', to: '2026-11-30' },
1782
+ },
1783
+ {
1784
+ name: 'slotlock_partner_reconcile_usage',
1785
+ title: 'Compare your counts with ours',
1786
+ description: "Send your count of each account's operations (and, if you keep them, bookings) on one ended UTC day, as your daily cross-check: the answer gives ours beside yours for every account either of us counted, the difference in percent, and whether every one is within 0.5%. A day outside that is recorded as a mismatch, which Pylota looks into. Sending the same day again replaces its record.",
1787
+ method: 'POST',
1788
+ path: '/v1/partner/usage/reconcile',
1789
+ input: z
1790
+ .object({
1791
+ day: USAGE_DAY,
1792
+ accounts: z
1793
+ .array(z.object({ account: PARTNER_ACCOUNT, operations: USAGE_COUNT, bookings: USAGE_COUNT.optional() }).strict())
1794
+ .max(10000),
1795
+ })
1796
+ .strict(),
1797
+ output: z
1798
+ .object({
1799
+ day: USAGE_DAY,
1800
+ within_tolerance: z.boolean(),
1801
+ tolerance_percent: z.number(),
1802
+ accounts: z
1803
+ .array(z
1804
+ .object({
1805
+ account: PARTNER_ACCOUNT,
1806
+ metric: z.enum(['operations', 'bookings']),
1807
+ ours: USAGE_COUNT,
1808
+ theirs: USAGE_COUNT.nullable(),
1809
+ difference_percent: z.number().nullable(),
1810
+ })
1811
+ .strict())
1812
+ .max(20000),
1813
+ })
1814
+ .strict(),
1815
+ risk: { readOnly: false, destructive: false, idempotent: true },
1816
+ invoke: 'partnerReconcileUsage',
1817
+ scope: 'partner',
1818
+ example: { day: '2026-11-09', accounts: [{ account: 'user-4821', operations: 1204 }] },
1819
+ },
1017
1820
  ]);
1018
1821
  const BY_NAME = new Map(SLOTLOCK_APPLICATION_OPERATIONS.map((operation) => [operation.name, operation]));
1019
1822
  export function isSlotlockApplicationOperation(name) {