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/engine.d.ts CHANGED
@@ -4,22 +4,19 @@ export declare function mergeIntervals(intervals: Interval[]): Interval[];
4
4
  /** Free sub-intervals of `window` once `busy` is carved out (busy need not be sorted/merged). */
5
5
  export declare function subtractBusy(window: Interval, busy: Interval[]): Interval[];
6
6
  /**
7
- * Earliest `[s, s + durationMs)` that fits entirely inside one of `windows` without touching
8
- * `busy`. Returns null when nothing fits (no windows, saturated, duration too long, or a
9
- * non-positive duration — a zero-width slot is never a useful answer).
7
+ * Earliest `[s, s + durationMs)` inside one of `windows` without touching `busy`, or null when
8
+ * nothing fits (including a non-positive duration).
10
9
  *
11
- * Adjacency is NOT a booking boundary: touching windows merge (half-open point-set semantics,
12
- * RFC 7953 set-union), so a slot may legitimately span the seam of two abutting rules. "Distinct
13
- * shifts, no straddling" is unexpressible via touching windows — model it as a gap or a future
14
- * constraint layer.
10
+ * Touching windows merge (RFC 7953 set-union), so a slot may span the seam of two abutting rules;
11
+ * "no straddling" must be modelled as a gap.
15
12
  */
16
13
  export declare function findNextAvailable(params: {
17
14
  busy: Interval[];
18
15
  windows: Interval[];
19
16
  durationMs: number;
20
17
  /**
21
- * Optional: the first allowed start at or after a candidate (a booking rule's step, say). It
22
- * must not move a start backwards.
18
+ * The first allowed start at or after a candidate (a booking rule's step); must not move a start
19
+ * backwards.
23
20
  */
24
21
  align?: (start: Date) => Date;
25
22
  }): Interval | null;
package/engine.js CHANGED
@@ -34,14 +34,11 @@ export function subtractBusy(window, busy) {
34
34
  return free;
35
35
  }
36
36
  /**
37
- * Earliest `[s, s + durationMs)` that fits entirely inside one of `windows` without touching
38
- * `busy`. Returns null when nothing fits (no windows, saturated, duration too long, or a
39
- * non-positive duration — a zero-width slot is never a useful answer).
37
+ * Earliest `[s, s + durationMs)` inside one of `windows` without touching `busy`, or null when
38
+ * nothing fits (including a non-positive duration).
40
39
  *
41
- * Adjacency is NOT a booking boundary: touching windows merge (half-open point-set semantics,
42
- * RFC 7953 set-union), so a slot may legitimately span the seam of two abutting rules. "Distinct
43
- * shifts, no straddling" is unexpressible via touching windows — model it as a gap or a future
44
- * constraint layer.
40
+ * Touching windows merge (RFC 7953 set-union), so a slot may span the seam of two abutting rules;
41
+ * "no straddling" must be modelled as a gap.
45
42
  */
46
43
  export function findNextAvailable(params) {
47
44
  const { busy, windows, durationMs } = params;
@@ -49,8 +46,7 @@ export function findNextAvailable(params) {
49
46
  return null;
50
47
  const slotAt = (start) => ({ start, end: new Date(start.getTime() + durationMs) });
51
48
  const align = params.align ?? ((start) => start);
52
- // One walk of the merged busy list alongside the merged windows, both by start: a busy interval
53
- // ending by a window's start ends before every later one too.
49
+ // One walk of merged busy alongside merged windows, both by start.
54
50
  const merged = mergeIntervals(busy);
55
51
  let next = 0;
56
52
  for (const window of mergeIntervals(windows)) {
package/event-feed.d.ts CHANGED
@@ -1,42 +1,36 @@
1
1
  import type { Sql, TransactionSql } from 'postgres';
2
2
  type SlotlockSql = Sql | TransactionSql;
3
3
  /** What a change can be, as the feed and a webhook's `event_types` name it. */
4
- export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "coverage.stale", "waitlist.offered", "waitlist.expired", "webhook.disabled", "resource.created", "resource.updated", "resource.archived", "resource.restored", "resource.deleted", "coverage.recorded", "coverage.deleted", "webhook.created", "webhook.enabled", "webhook.deleted", "key.created", "key.rotated", "key.revoked"];
4
+ export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "coverage.stale", "waitlist.offered", "waitlist.expired", "approval.requested", "approval.granted", "approval.declined", "approval.expired", "webhook.disabled", "usage.threshold_reached", "resource.created", "resource.updated", "resource.archived", "resource.restored", "resource.deleted", "coverage.recorded", "coverage.deleted", "webhook.created", "webhook.enabled", "webhook.deleted", "key.created", "key.rotated", "key.revoked"];
5
5
  export type SlotlockChangeType = (typeof SLOTLOCK_CHANGE_TYPES)[number];
6
6
  /**
7
- * Where a change came from, as the history and the feed record it (withTenant's `origin`): an agent
8
- * protocol (`mcp`, `a2a`), the REST API's agent routes (`rest`) or its application operations
9
- * (`application`), the dashboard, a member's dashboard assistant, maintenance, or code calling the
10
- * store itself (`library`). Never an address or anything else about a person.
7
+ * Where a change came from (withTenant's `origin`): an agent protocol (`mcp`, `a2a`), the REST
8
+ * API's agent routes (`rest`) or application operations (`application`), the dashboard, its
9
+ * assistant, maintenance, or direct store calls (`library`). Never anything about a person.
11
10
  */
12
11
  export declare const SLOTLOCK_CHANGE_ORIGINS: readonly ["mcp", "a2a", "rest", "application", "dashboard", "dashboard-assistant", "maintenance", "library"];
13
12
  export type SlotlockChangeOrigin = (typeof SLOTLOCK_CHANGE_ORIGINS)[number];
14
13
  /** Whether `value` is one of SLOTLOCK_CHANGE_ORIGINS. */
15
14
  export declare function isSlotlockChangeOrigin(value: unknown): value is SlotlockChangeOrigin;
16
15
  /**
17
- * The digest 0.5.3 named a coverage source by on the feed: `src_` and the first 24 hex digits of
18
- * sha256(tenant, newline, source). Unkeyed, so anyone who knows the tenant could confirm a guessed
19
- * source against it; 0.6 names a source by a random id instead (`calendar_coverage.source_id`), and
20
- * the upgrade uses this only to find the changes 0.5.3 wrote and rename them. `tenant` and `source`
21
- * are SQL expressions.
16
+ * The unkeyed digest 0.5.3 named a coverage source by (`src_` + 24 hex of sha256(tenant, newline,
17
+ * source)); anyone knowing the tenant could confirm a guess. Used only by the upgrade to find and
18
+ * rename those changes. `tenant` and `source` are SQL expressions.
22
19
  */
23
20
  export declare function legacyCoverageSourceDigestSql(tenant: string, source: string): string;
24
21
  /**
25
- * 0.5.3 worked out a source's id here: an unkeyed digest of the tenant and the source, which let
26
- * anyone who saw a change confirm a guessed source. Since 0.6 a source's `source_id` is random and
27
- * only the database knows it, so this throws (`coverage_source_id_unavailable`) rather than give a
28
- * digest nothing carries any more.
22
+ * Throws `coverage_source_id_unavailable`: since 0.6 a source's `source_id` is random and only the
23
+ * database knows it (0.5.3's unkeyed digest let anyone confirm a guessed source).
29
24
  *
30
25
  * @deprecated Read `sourceId` from `listCoverage` (`source_id` of `slotlock_get_coverage`). It may
31
26
  * be removed in 0.7.0.
32
27
  */
33
28
  export declare function slotlockCoverageSourceId(_tenantRef: string, _source: string): never;
34
29
  /**
35
- * A change's subject and data as the feed serves them. A `coverage.*` change never carries its
36
- * source (`source`) or the error read from it (`last_error`, which can quote it), and is named by a
37
- * `source_id` of this release or `src_redacted`: an earlier release wrote both, and the upgrade
38
- * renames those rows in batches after it commits, so a row it has not reached yet, or one an
39
- * earlier release's server writes during a rolling upgrade, is served redacted.
30
+ * A change's subject and data as the feed serves them. A `coverage.*` change never carries `source`
31
+ * or `last_error` (which can quote it), and is named by a current `source_id` or `src_redacted`:
32
+ * rows an earlier release wrote are renamed in batches after upgrade, so one not reached yet is
33
+ * served redacted.
40
34
  */
41
35
  export declare function slotlockServedChange(type: string, subject: string, data: Record<string, unknown>): {
42
36
  subject: string;
@@ -45,7 +39,7 @@ export declare function slotlockServedChange(type: string, subject: string, data
45
39
  /** How long the feed keeps a change, and so how far back a cursor may reach. */
46
40
  export declare const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
47
41
  /** Webhook endpoints one tenant may have. */
48
- export declare const SLOTLOCK_MAX_WEBHOOK_ENDPOINTS = 10;
42
+ export declare const SLOTLOCK_MAX_WEBHOOK_ENDPOINTS = 25;
49
43
  /** After this long failing, an endpoint is disabled and the feed says so (`webhook.disabled`). */
50
44
  export declare const SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS: number;
51
45
  export interface SlotlockChange {
@@ -66,11 +60,29 @@ export interface SlotlockChange {
66
60
  /** Pass as `after` to read what follows this change. */
67
61
  position: string;
68
62
  }
63
+ /** Resources one webhook endpoint's filter may name. */
64
+ export declare const SLOTLOCK_WEBHOOK_FILTER_RESOURCES = 100;
65
+ /** Pools, locations or groups one webhook endpoint's filter may name, of each. */
66
+ export declare const SLOTLOCK_WEBHOOK_FILTER_REFS = 20;
67
+ /**
68
+ * Which resources' changes an endpoint receives (RFC 0003 G5): only changes about a resource that
69
+ * matches one of these, as it is when the change is delivered, so a change about no resource (a key,
70
+ * a webhook, a usage notice) is not sent. A location includes every location in it. At most
71
+ * SLOTLOCK_WEBHOOK_FILTER_RESOURCES ids, and SLOTLOCK_WEBHOOK_FILTER_REFS of each of the others.
72
+ */
73
+ export interface SlotlockWebhookFilter {
74
+ resourceIds?: string[];
75
+ pools?: string[];
76
+ locations?: string[];
77
+ groups?: string[];
78
+ }
69
79
  export interface SlotlockWebhookEndpoint {
70
80
  id: string;
71
81
  url: string;
72
82
  /** Null: every type. */
73
83
  eventTypes: SlotlockChangeType[] | null;
84
+ /** Only changes about these resources; absent, changes about any resource or none. */
85
+ filter?: SlotlockWebhookFilter;
74
86
  createdAt: Date;
75
87
  /** Failed attempts since the last delivery that succeeded. */
76
88
  attempts: number;
@@ -96,10 +108,11 @@ export interface SlotlockWebhookClaim {
96
108
  export type CreateWebhookEndpointResult = {
97
109
  ok: true;
98
110
  endpoint: SlotlockWebhookEndpoint;
99
- secret: string;
111
+ secret: string; /** True when an idempotency key answered what an earlier create made. */
112
+ replayed?: boolean;
100
113
  } | {
101
114
  ok: false;
102
- code: 'invalid_webhook_url' | 'invalid_event_types';
115
+ code: 'invalid_webhook_url' | 'invalid_event_types' | 'invalid_webhook_filter' | 'idempotency_conflict' | 'invalid_identity';
103
116
  } | {
104
117
  ok: false;
105
118
  code: 'webhook_limit_reached';
@@ -115,11 +128,10 @@ export type ListChangesResult = {
115
128
  };
116
129
  export interface SlotlockEventFeed {
117
130
  /**
118
- * The tenant's changes after `after` (a `position`, or the oldest kept without it), oldest
119
- * first, up to `limit` (1-500, default 100). Only changes of transactions that have ended are
120
- * read, so a reader that follows `next` never skips one that commits late. `next` is the last
121
- * change's position, or `after` when there is none yet: poll with it. A position before a change
122
- * any store has pruned is `cursor_expired`: read from the start again.
131
+ * The tenant's changes after `after` (or from the oldest kept), oldest first, up to `limit`
132
+ * (1-500, default 100). Only ended transactions are read, so a reader following `next` never
133
+ * skips a late commit. `next` is the last position, or `after` when there is none: poll with it.
134
+ * A position before a pruned change is `cursor_expired`.
123
135
  */
124
136
  listChanges(params: {
125
137
  tenantRef: string;
@@ -143,6 +155,13 @@ export interface SlotlockEventFeed {
143
155
  tenantRef: string;
144
156
  url: string;
145
157
  eventTypes?: readonly string[];
158
+ /** Only changes about these resources (`invalid_webhook_filter` past its bounds). */
159
+ filter?: SlotlockWebhookFilter;
160
+ /**
161
+ * A retry identity, kept as long as the endpoint: the same key and request answer the endpoint
162
+ * and secret the first made (`replayed`), another request `idempotency_conflict`.
163
+ */
164
+ idempotencyKey?: string;
146
165
  }): Promise<CreateWebhookEndpointResult>;
147
166
  listWebhookEndpoints(params: {
148
167
  tenantRef: string;
@@ -165,9 +184,9 @@ export interface SlotlockEventFeed {
165
184
  code: 'changes_pruned';
166
185
  }>;
167
186
  /**
168
- * Lease the tenant's endpoints that have changes due, each with its next `batch` (1-100)
169
- * changes, for `leaseMs` (1,000-300,000). A lease that runs out is claimed again by any worker,
170
- * so a worker that dies mid-batch costs a redelivery, never a lost change.
187
+ * Lease the tenant's endpoints with changes due, each with its next `batch` (1-100) changes, for
188
+ * `leaseMs` (1,000-300,000). An expired lease is claimed again by any worker, so a crash costs a
189
+ * redelivery, never a lost change.
171
190
  */
172
191
  claimWebhookDeliveries(params: {
173
192
  tenantRef: string;
@@ -193,11 +212,10 @@ export interface SlotlockEventFeed {
193
212
  };
194
213
  }): Promise<void>;
195
214
  /**
196
- * History: the changes to one resource (`resourceId`: its bookings and events, and its own
197
- * configuration) or one record (`subject`: a booking's reference, an event's, hold's, webhook's or
198
- * API key's id), newest first, `limit` (1-100) at a time, each with who made it (`actor`, `keyId`),
199
- * where from (`origin`) and, for a change of an existing record, what it was before. Pass the last one's
200
- * `id` as `before` for the page after. Kept as long as the feed keeps changes.
215
+ * History of one resource (`resourceId`: its bookings, events and configuration) or one record
216
+ * (`subject`), newest first, `limit` (1-100) at a time, each with `actor`, `keyId`, `origin` and
217
+ * the prior state where there was one. Pass the last `id` as `before` for the next page. Kept as
218
+ * long as the feed.
201
219
  */
202
220
  listHistory(params: {
203
221
  tenantRef: string;