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.
- package/CHANGELOG.md +492 -0
- package/CONTRIBUTING.md +12 -5
- package/README.md +50 -1544
- package/SECURITY.md +5 -2
- package/SPEC.md +1 -1
- package/agent-server.d.ts +160 -8
- package/agent-server.js +560 -49
- package/agent-store-backend.d.ts +39 -7
- package/agent-store-backend.js +1439 -104
- package/api-keys.d.ts +69 -3
- package/api-keys.js +218 -29
- package/application-api.d.ts +59 -6
- package/application-api.js +842 -39
- package/approvals.d.ts +88 -0
- package/approvals.js +293 -0
- package/assistant.d.ts +58 -27
- package/assistant.js +151 -53
- package/billing.d.ts +114 -0
- package/billing.js +258 -0
- package/booking-rules.d.ts +30 -41
- package/booking-rules.js +17 -25
- package/change-watch.d.ts +1 -0
- package/change-watch.js +1 -1
- package/cli.js +3 -4
- package/config.d.ts +42 -6
- package/config.js +114 -7
- package/dashboard-icon.d.ts +1 -0
- package/dashboard-icon.js +5 -0
- package/dashboard.d.ts +203 -7
- package/dashboard.js +1558 -64
- package/ddl.d.ts +67 -35
- package/ddl.js +2732 -162
- package/engine.d.ts +6 -9
- package/engine.js +5 -9
- package/event-feed.d.ts +53 -35
- package/event-feed.js +201 -64
- package/event-schema.js +6 -9
- package/hooks.d.ts +2 -4
- package/hooks.js +2 -4
- package/ical-worker-thread.js +3 -4
- package/ical-worker.d.ts +8 -10
- package/ical-worker.js +7 -9
- package/index.d.ts +15 -4
- package/index.js +8 -2
- package/interop.js +1 -4
- package/maintenance.d.ts +24 -10
- package/maintenance.js +59 -28
- package/mcp-app.d.ts +7 -9
- package/mcp-app.js +7 -9
- package/mcp-modern.d.ts +14 -16
- package/mcp-modern.js +23 -27
- package/metrics.d.ts +41 -0
- package/metrics.js +22 -1
- package/node-server.d.ts +9 -9
- package/node-server.js +25 -34
- package/openapi.json +14675 -5957
- package/otel.d.ts +86 -0
- package/otel.js +273 -0
- package/overage.d.ts +33 -0
- package/overage.js +115 -0
- package/package-version.js +2 -4
- package/package.json +1 -1
- package/partner-billing.d.ts +81 -0
- package/partner-billing.js +333 -0
- package/partner-names.d.ts +8 -0
- package/partner-names.js +10 -0
- package/partners.d.ts +147 -0
- package/partners.js +375 -0
- package/plans.d.ts +92 -0
- package/plans.js +115 -0
- package/quotas.d.ts +108 -0
- package/quotas.js +258 -0
- package/rate-limit.d.ts +27 -2
- package/rate-limit.js +106 -23
- package/resource-profile.d.ts +135 -0
- package/resource-profile.js +263 -0
- package/rest-api.d.ts +15 -14
- package/rest-api.js +79 -15
- package/rules.d.ts +6 -7
- package/rules.js +30 -42
- package/sandbox.d.ts +70 -0
- package/sandbox.js +295 -0
- package/self-host.d.ts +46 -4
- package/self-host.js +990 -41
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +482 -165
- package/store.js +1460 -484
- package/sync.d.ts +44 -59
- package/sync.js +325 -469
- package/teams.d.ts +25 -0
- package/teams.js +63 -0
- package/timezone.d.ts +25 -33
- package/timezone.js +50 -53
- package/types.d.ts +130 -57
- package/usage.d.ts +106 -0
- package/usage.js +238 -0
- package/webhooks.d.ts +22 -25
- package/webhooks.js +35 -40
- package/weekly-hours.d.ts +3 -0
- 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
|
-
*
|
|
21
|
-
*
|
|
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
|
|
26
|
-
* secret's bytes
|
|
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
|
-
*
|
|
36
|
-
*
|
|
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,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
*
|
|
17
|
-
*
|
|
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
|
|
36
|
-
* secret's bytes
|
|
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
|
-
*
|
|
49
|
-
*
|
|
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
|
|
59
|
-
//
|
|
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,
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
//
|
|
132
|
-
//
|
|
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
|
|
156
|
-
//
|
|
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
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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
|
-
//
|
|
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
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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
|
-
//
|
|
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
|
|
332
|
-
*
|
|
333
|
-
*
|
|
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
|
|
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
|
/**
|