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/webhooks.d.ts CHANGED
@@ -17,13 +17,13 @@ export interface SlotlockCloudEvent {
17
17
  }
18
18
  /**
19
19
  * A change as CloudEvents 1.0 (structured mode): `type` is `io.pylota.slotlock.<change type>`, with
20
- * the extension attributes `slotlockorigin` and `slotlockkeyid` when the change records them. A
21
- * `coverage.*` change is redacted as the feed serves it (slotlockServedChange), whoever built it.
20
+ * `slotlockorigin` and `slotlockkeyid` when recorded. A `coverage.*` change is redacted as the feed
21
+ * serves it (slotlockServedChange).
22
22
  */
23
23
  export declare function slotlockCloudEvent(change: SlotlockChange): SlotlockCloudEvent;
24
24
  /**
25
- * The Standard Webhooks signature of one attempt: `v1,` and the base64 HMAC-SHA256, keyed by the
26
- * secret's bytes, of `id.timestamp.body`.
25
+ * The Standard Webhooks signature: `v1,` and the base64 HMAC-SHA256 of `id.timestamp.body`, keyed
26
+ * by the secret's bytes.
27
27
  */
28
28
  export declare function signSlotlockWebhook(params: {
29
29
  secret: string;
@@ -32,9 +32,8 @@ export declare function signSlotlockWebhook(params: {
32
32
  body: string;
33
33
  }): string;
34
34
  /**
35
- * When to try a failed endpoint again, by how many attempts have failed in a row: the Standard
36
- * Webhooks example schedule (5 s, 5 min, 30 min, 2 h, 5 h, then 10 h), with ±10% jitter, and never
37
- * sooner than the endpoint's Retry-After (capped at a day).
35
+ * Retry delay by consecutive failures: the Standard Webhooks example schedule (5 s, 5 min, 30 min,
36
+ * 2 h, 5 h, then 10 h) with ±10% jitter, never sooner than Retry-After (capped at a day).
38
37
  */
39
38
  export declare function slotlockWebhookRetryDelayMs(attempts: number, retryAfterMs?: number, random?: () => number): number;
40
39
  /** Whether `address` is one a webhook may not reach: loopback, private, link-local, and the like. */
@@ -50,19 +49,18 @@ export type SlotlockWebhookSender = (request: {
50
49
  body: string;
51
50
  }) => Promise<SlotlockWebhookResponse>;
52
51
  /**
53
- * POST with Node's http client: no redirect is followed, the answer's body is never read beyond
54
- * 64 KiB, and every address the host resolves to is checked when the socket connects, so a name
55
- * that resolves to a private network (or is rebound to one) is refused unless `allowPrivateNetworks`.
52
+ * POST with Node's http client: no redirect is followed, at most 64 KiB of body is read, and every
53
+ * resolved address is checked at connect time, so a name resolving (or rebound) to a private
54
+ * network is refused unless `allowPrivateNetworks`.
56
55
  */
57
56
  export declare function createSlotlockWebhookSender(options?: {
58
57
  timeoutMs?: number;
59
58
  allowPrivateNetworks?: boolean;
60
59
  }): SlotlockWebhookSender;
61
60
  /**
62
- * A `dns.lookup` that answers every address a name resolves to, and refuses (an error with `code`)
63
- * a name with any private one unless `allowPrivateNetworks`. Given to a socket's connect, it checks
64
- * the addresses the connection will use, so a name rebound to a private address after an earlier
65
- * check is still refused. `resolve` stands in for `dns.lookup` (tests).
61
+ * A `dns.lookup` that returns every address and refuses (with `code`) a name with any private one
62
+ * unless `allowPrivateNetworks`. Used at socket connect, so a rebound name is still refused.
63
+ * `resolve` stands in for `dns.lookup` in tests.
66
64
  */
67
65
  export declare function slotlockPrivateAddressLookup(options: {
68
66
  allowPrivateNetworks: boolean;
@@ -71,11 +69,10 @@ export declare function slotlockPrivateAddressLookup(options: {
71
69
  resolve?: typeof dnsLookup;
72
70
  }): typeof dnsLookup;
73
71
  /**
74
- * A `fetch` for a server reaching a URL a tenant chose (the dashboard assistant's model): Node's
75
- * http client with the webhooks' address check. A URL naming a private address, or a host that
76
- * resolves to one when the socket connects, is refused with an error whose `code` is
77
- * `private_address`, unless `allowPrivateNetworks`. No redirect is followed (the answer is
78
- * returned as is) and no proxy from the environment is used. Takes a string body only.
72
+ * A `fetch` for URLs a tenant chose (the assistant's model), with the webhooks' address check: a
73
+ * private address or a host resolving to one at connect time is refused with `code`
74
+ * `private_address` unless `allowPrivateNetworks`. No redirects, no environment proxy, string
75
+ * bodies only.
79
76
  */
80
77
  export declare function createSlotlockGuardedFetch(options?: {
81
78
  allowPrivateNetworks?: boolean;
@@ -88,9 +85,9 @@ export interface SlotlockWebhookPassResult {
88
85
  failed: number;
89
86
  }
90
87
  /**
91
- * One delivery pass for a tenant: claim each endpoint with changes due, send them in order until
92
- * one fails, and record how far each got. A 2xx is delivered; 410 disables the endpoint; anything
93
- * else, or no answer, is retried on the schedule. The send happens outside any transaction.
88
+ * One delivery pass for a tenant: claim each endpoint with changes due, send in order until one
89
+ * fails, and record progress. 2xx is delivered; 410 disables the endpoint; anything else is retried
90
+ * on the schedule. Sending happens outside any transaction.
94
91
  */
95
92
  export declare function deliverSlotlockWebhooks(params: {
96
93
  feed: Pick<SlotlockEventFeed, 'claimWebhookDeliveries' | 'settleWebhookDelivery'>;
@@ -108,9 +105,9 @@ export interface SlotlockWebhookScheduler {
108
105
  close(): Promise<void>;
109
106
  }
110
107
  /**
111
- * Delivery passes every `intervalMs` (250-60,000, default 2,000) for the tenants `tenants()` lists
112
- * (the store's webhook_tenants(), say) and those touched, one tenant at a time. A pass that fails
113
- * is reported and the next one runs as usual.
108
+ * Delivery passes every `intervalMs` (250-60,000, default 2,000) for `tenants()` (the store's
109
+ * webhook_tenants(), say) and touched tenants. A failed pass is reported and the next runs as
110
+ * usual.
114
111
  */
115
112
  export declare function startSlotlockWebhookDelivery(options: {
116
113
  tenants: () => Promise<readonly string[]>;
package/webhooks.js CHANGED
@@ -1,8 +1,7 @@
1
- // Webhook delivery of the event feed (RFC 0002 P1-5): each change is a CloudEvents 1.0 envelope,
2
- // signed and headed as Standard Webhooks asks (webhook-id, webhook-timestamp, webhook-signature),
3
- // sent to each endpoint in feed order, one at a time, and retried on a growing schedule until the
4
- // endpoint has failed for a day. The store keeps the position and the schedule (event-feed.ts), so
5
- // a worker that stops mid-batch loses nothing: the next one sends the rest, with the same ids.
1
+ // Webhook delivery of the event feed: each change is a CloudEvents 1.0 envelope, signed with
2
+ // Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature), sent per endpoint
3
+ // in feed order, and retried on a growing schedule until the endpoint has failed for a day. The
4
+ // store keeps position and schedule (event-feed.ts), so a stopped worker loses nothing.
6
5
  import { createHmac } from 'node:crypto';
7
6
  import { lookup as dnsLookup } from 'node:dns';
8
7
  import { Agent as HttpAgent, request as httpRequest } from 'node:http';
@@ -13,8 +12,8 @@ import { slotlockServedChange, } from './event-feed.js';
13
12
  import { ignoreRejection } from './hooks.js';
14
13
  /**
15
14
  * A change as CloudEvents 1.0 (structured mode): `type` is `io.pylota.slotlock.<change type>`, with
16
- * the extension attributes `slotlockorigin` and `slotlockkeyid` when the change records them. A
17
- * `coverage.*` change is redacted as the feed serves it (slotlockServedChange), whoever built it.
15
+ * `slotlockorigin` and `slotlockkeyid` when recorded. A `coverage.*` change is redacted as the feed
16
+ * serves it (slotlockServedChange).
18
17
  */
19
18
  export function slotlockCloudEvent(change) {
20
19
  const served = slotlockServedChange(change.type, change.subject, change.data);
@@ -32,8 +31,8 @@ export function slotlockCloudEvent(change) {
32
31
  };
33
32
  }
34
33
  /**
35
- * The Standard Webhooks signature of one attempt: `v1,` and the base64 HMAC-SHA256, keyed by the
36
- * secret's bytes, of `id.timestamp.body`.
34
+ * The Standard Webhooks signature: `v1,` and the base64 HMAC-SHA256 of `id.timestamp.body`, keyed
35
+ * by the secret's bytes.
37
36
  */
38
37
  export function signSlotlockWebhook(params) {
39
38
  if (!params.secret.startsWith('whsec_'))
@@ -45,9 +44,8 @@ export function signSlotlockWebhook(params) {
45
44
  return `v1,${signature}`;
46
45
  }
47
46
  /**
48
- * When to try a failed endpoint again, by how many attempts have failed in a row: the Standard
49
- * Webhooks example schedule (5 s, 5 min, 30 min, 2 h, 5 h, then 10 h), with ±10% jitter, and never
50
- * sooner than the endpoint's Retry-After (capped at a day).
47
+ * Retry delay by consecutive failures: the Standard Webhooks example schedule (5 s, 5 min, 30 min,
48
+ * 2 h, 5 h, then 10 h) with ±10% jitter, never sooner than Retry-After (capped at a day).
51
49
  */
52
50
  export function slotlockWebhookRetryDelayMs(attempts, retryAfterMs, random = Math.random) {
53
51
  const schedule = [5_000, 300_000, 1_800_000, 7_200_000, 18_000_000, 36_000_000];
@@ -55,8 +53,8 @@ export function slotlockWebhookRetryDelayMs(attempts, retryAfterMs, random = Mat
55
53
  const jittered = Math.round(base * (0.9 + random() * 0.2));
56
54
  return Math.max(jittered, Math.min(retryAfterMs ?? 0, 86_400_000));
57
55
  }
58
- // One list per family: a BlockList also checks an IPv4 address against IPv6 rules through its
59
- // mapped form, so ::ffff:0:0/96 in the same list would match every IPv4 address.
56
+ // One list per family: a BlockList checks IPv4 against IPv6 rules via the mapped form, so
57
+ // ::ffff:0:0/96 in the same list would match every IPv4 address.
60
58
  const PRIVATE_IPV4 = new BlockList();
61
59
  for (const [address, prefix] of [
62
60
  ['0.0.0.0', 8],
@@ -95,7 +93,6 @@ export function isPrivateAddress(address) {
95
93
  return PRIVATE_IPV4.check(address, 'ipv4');
96
94
  if (family !== 6)
97
95
  return true;
98
- // An IPv4-mapped address (::ffff:a.b.c.d, or its hex form) is the IPv4 address it carries.
99
96
  const mapped = /^::ffff:(?:(\d+\.\d+\.\d+\.\d+)|([0-9a-f]{1,4}):([0-9a-f]{1,4}))$/i.exec(address);
100
97
  if (mapped) {
101
98
  if (mapped[1])
@@ -107,9 +104,9 @@ export function isPrivateAddress(address) {
107
104
  return PRIVATE_IPV6.check(address, 'ipv6');
108
105
  }
109
106
  /**
110
- * POST with Node's http client: no redirect is followed, the answer's body is never read beyond
111
- * 64 KiB, and every address the host resolves to is checked when the socket connects, so a name
112
- * that resolves to a private network (or is rebound to one) is refused unless `allowPrivateNetworks`.
107
+ * POST with Node's http client: no redirect is followed, at most 64 KiB of body is read, and every
108
+ * resolved address is checked at connect time, so a name resolving (or rebound) to a private
109
+ * network is refused unless `allowPrivateNetworks`.
113
110
  */
114
111
  export function createSlotlockWebhookSender(options = {}) {
115
112
  const timeoutMs = options.timeoutMs ?? 15_000;
@@ -128,8 +125,8 @@ export function createSlotlockWebhookSender(options = {}) {
128
125
  const outgoing = send(url, {
129
126
  method: 'POST',
130
127
  headers: { ...request.headers, 'Content-Length': String(Buffer.byteLength(request.body)) },
131
- // An agent of its own, never the global one: with NODE_USE_ENV_PROXY a proxy from the
132
- // environment would connect for us, and the address check below would never run.
128
+ // Our own agent, never the global one: with NODE_USE_ENV_PROXY a proxy would connect
129
+ // instead and the address check would never run.
133
130
  agent: url.protocol === 'https:' ? new HttpsAgent({ lookup }) : new HttpAgent({ lookup }),
134
131
  lookup,
135
132
  timeout: timeoutMs,
@@ -152,8 +149,8 @@ export function createSlotlockWebhookSender(options = {}) {
152
149
  });
153
150
  const timedOut = () => outgoing.destroy(Object.assign(new Error('webhook request timed out'), { code: 'webhook_timeout' }));
154
151
  outgoing.on('timeout', timedOut);
155
- // `timeout` is idleness only: an answer dripped a byte at a time would hold the request
156
- // open for hours. The whole exchange has this long.
152
+ // `timeout` is idleness only, so a dripped answer could hold the request for hours: the whole
153
+ // exchange gets this long.
157
154
  const deadline = setTimeout(timedOut, timeoutMs);
158
155
  deadline.unref?.();
159
156
  outgoing.on('close', () => clearTimeout(deadline));
@@ -167,10 +164,9 @@ function namesPrivateAddress(url) {
167
164
  return isIP(host) !== 0 && isPrivateAddress(host);
168
165
  }
169
166
  /**
170
- * A `dns.lookup` that answers every address a name resolves to, and refuses (an error with `code`)
171
- * a name with any private one unless `allowPrivateNetworks`. Given to a socket's connect, it checks
172
- * the addresses the connection will use, so a name rebound to a private address after an earlier
173
- * check is still refused. `resolve` stands in for `dns.lookup` (tests).
167
+ * A `dns.lookup` that returns every address and refuses (with `code`) a name with any private one
168
+ * unless `allowPrivateNetworks`. Used at socket connect, so a rebound name is still refused.
169
+ * `resolve` stands in for `dns.lookup` in tests.
174
170
  */
175
171
  export function slotlockPrivateAddressLookup(options) {
176
172
  const resolve = options.resolve ?? dnsLookup;
@@ -191,11 +187,10 @@ export function slotlockPrivateAddressLookup(options) {
191
187
  });
192
188
  }
193
189
  /**
194
- * A `fetch` for a server reaching a URL a tenant chose (the dashboard assistant's model): Node's
195
- * http client with the webhooks' address check. A URL naming a private address, or a host that
196
- * resolves to one when the socket connects, is refused with an error whose `code` is
197
- * `private_address`, unless `allowPrivateNetworks`. No redirect is followed (the answer is
198
- * returned as is) and no proxy from the environment is used. Takes a string body only.
190
+ * A `fetch` for URLs a tenant chose (the assistant's model), with the webhooks' address check: a
191
+ * private address or a host resolving to one at connect time is refused with `code`
192
+ * `private_address` unless `allowPrivateNetworks`. No redirects, no environment proxy, string
193
+ * bodies only.
199
194
  */
200
195
  export function createSlotlockGuardedFetch(options = {}) {
201
196
  const allow = options.allowPrivateNetworks === true;
@@ -224,7 +219,7 @@ export function createSlotlockGuardedFetch(options = {}) {
224
219
  const outgoing = send(url, {
225
220
  method: init.method ?? 'GET',
226
221
  headers,
227
- // An agent of its own: a proxy from the environment would skip the address check.
222
+ // Our own agent: an environment proxy would skip the address check.
228
223
  agent: url.protocol === 'https:' ? new HttpsAgent({ lookup }) : new HttpAgent({ lookup }),
229
224
  lookup,
230
225
  ...(init.signal ? { signal: init.signal } : {}),
@@ -250,9 +245,9 @@ export function createSlotlockGuardedFetch(options = {}) {
250
245
  });
251
246
  }
252
247
  /**
253
- * One delivery pass for a tenant: claim each endpoint with changes due, send them in order until
254
- * one fails, and record how far each got. A 2xx is delivered; 410 disables the endpoint; anything
255
- * else, or no answer, is retried on the schedule. The send happens outside any transaction.
248
+ * One delivery pass for a tenant: claim each endpoint with changes due, send in order until one
249
+ * fails, and record progress. 2xx is delivered; 410 disables the endpoint; anything else is retried
250
+ * on the schedule. Sending happens outside any transaction.
256
251
  */
257
252
  export async function deliverSlotlockWebhooks(params) {
258
253
  const now = params.now ?? Date.now;
@@ -267,7 +262,7 @@ export async function deliverSlotlockWebhooks(params) {
267
262
  await Promise.all(claims.map(async (claim) => {
268
263
  let last;
269
264
  let failure;
270
- // The failures before this pass set the delay: the schedule grows across passes.
265
+ // Failures before this pass set the delay, so the schedule grows across passes.
271
266
  let attempts = claim.endpoint.attempts;
272
267
  for (const change of claim.changes) {
273
268
  if (now() >= stopAt)
@@ -328,9 +323,9 @@ export async function deliverSlotlockWebhooks(params) {
328
323
  return { delivered, failed };
329
324
  }
330
325
  /**
331
- * Delivery passes every `intervalMs` (250-60,000, default 2,000) for the tenants `tenants()` lists
332
- * (the store's webhook_tenants(), say) and those touched, one tenant at a time. A pass that fails
333
- * is reported and the next one runs as usual.
326
+ * Delivery passes every `intervalMs` (250-60,000, default 2,000) for `tenants()` (the store's
327
+ * webhook_tenants(), say) and touched tenants. A failed pass is reported and the next runs as
328
+ * usual.
334
329
  */
335
330
  export function startSlotlockWebhookDelivery(options) {
336
331
  const intervalMs = options.intervalMs ?? 2_000;
@@ -361,7 +356,7 @@ export function startSlotlockWebhookDelivery(options) {
361
356
  await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, worker));
362
357
  };
363
358
  const timer = setInterval(() => {
364
- // A pass still running skips this tick rather than queue another behind it.
359
+ // A pass still running skips this tick.
365
360
  if (running || closed)
366
361
  return;
367
362
  running = pass()
package/weekly-hours.d.ts CHANGED
@@ -13,6 +13,9 @@ export interface HoursWindow {
13
13
  to: number;
14
14
  }
15
15
  export type WeeklyHours = Record<Weekday, HoursWindow[]>;
16
+ export declare function invalidAvailability(message: string): Error & {
17
+ code: 'invalid_availability';
18
+ };
16
19
  /**
17
20
  * Why `candidate` cannot be an availability rule: `'shape'` (not exactly rrule, startMinutes 0-1439
18
21
  * and durationMinutes 1-10080), `'unsupported'` (a rule `expandRules` would skip: anything but
package/weekly-hours.js CHANGED
@@ -23,7 +23,7 @@ const MAX_WINDOWS_A_DAY = 2;
23
23
  const CANONICAL_RRULE = /^FREQ=WEEKLY;BYDAY=((?:MO|TU|WE|TH|FR|SA|SU)(?:,(?:MO|TU|WE|TH|FR|SA|SU))*)$/;
24
24
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
25
25
  const isMinuteOfDay = (value) => Number.isInteger(value) && value >= 0 && value < DAY_MINUTES;
26
- function invalidAvailability(message) {
26
+ export function invalidAvailability(message) {
27
27
  return Object.assign(new Error(message), { code: 'invalid_availability' });
28
28
  }
29
29
  /**