slotlock 0.6.3 → 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 +467 -0
- package/CONTRIBUTING.md +12 -5
- package/README.md +50 -1551
- package/SECURITY.md +5 -2
- package/SPEC.md +1 -1
- package/agent-server.d.ts +160 -8
- package/agent-server.js +559 -44
- package/agent-store-backend.d.ts +34 -1
- package/agent-store-backend.js +1399 -38
- package/api-keys.d.ts +69 -3
- package/api-keys.js +217 -28
- package/application-api.d.ts +56 -3
- package/application-api.js +826 -15
- package/approvals.d.ts +88 -0
- package/approvals.js +293 -0
- package/assistant.d.ts +40 -0
- package/assistant.js +115 -0
- package/billing.d.ts +114 -0
- package/billing.js +258 -0
- package/config.d.ts +25 -4
- package/config.js +72 -3
- package/dashboard-icon.d.ts +1 -0
- package/dashboard-icon.js +5 -0
- package/dashboard.d.ts +203 -7
- package/dashboard.js +1545 -64
- package/ddl.d.ts +51 -12
- package/ddl.js +2566 -54
- package/event-feed.d.ts +30 -4
- package/event-feed.js +171 -19
- package/index.d.ts +15 -4
- package/index.js +8 -2
- package/maintenance.d.ts +10 -0
- package/maintenance.js +16 -2
- package/metrics.d.ts +41 -0
- package/metrics.js +22 -0
- 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.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 +21 -1
- package/rate-limit.js +82 -15
- package/resource-profile.d.ts +135 -0
- package/resource-profile.js +263 -0
- package/rest-api.d.ts +2 -0
- package/rest-api.js +68 -3
- package/sandbox.d.ts +70 -0
- package/sandbox.js +295 -0
- package/self-host.d.ts +33 -5
- package/self-host.js +950 -22
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +373 -4
- package/store.js +1212 -118
- package/teams.d.ts +25 -0
- package/teams.js +63 -0
- package/timezone.js +13 -1
- package/types.d.ts +95 -1
- package/usage.d.ts +106 -0
- package/usage.js +238 -0
package/event-feed.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
7
|
* Where a change came from (withTenant's `origin`): an agent protocol (`mcp`, `a2a`), the REST
|
|
@@ -39,7 +39,7 @@ export declare function slotlockServedChange(type: string, subject: string, data
|
|
|
39
39
|
/** How long the feed keeps a change, and so how far back a cursor may reach. */
|
|
40
40
|
export declare const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
|
|
41
41
|
/** Webhook endpoints one tenant may have. */
|
|
42
|
-
export declare const SLOTLOCK_MAX_WEBHOOK_ENDPOINTS =
|
|
42
|
+
export declare const SLOTLOCK_MAX_WEBHOOK_ENDPOINTS = 25;
|
|
43
43
|
/** After this long failing, an endpoint is disabled and the feed says so (`webhook.disabled`). */
|
|
44
44
|
export declare const SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS: number;
|
|
45
45
|
export interface SlotlockChange {
|
|
@@ -60,11 +60,29 @@ export interface SlotlockChange {
|
|
|
60
60
|
/** Pass as `after` to read what follows this change. */
|
|
61
61
|
position: string;
|
|
62
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
|
+
}
|
|
63
79
|
export interface SlotlockWebhookEndpoint {
|
|
64
80
|
id: string;
|
|
65
81
|
url: string;
|
|
66
82
|
/** Null: every type. */
|
|
67
83
|
eventTypes: SlotlockChangeType[] | null;
|
|
84
|
+
/** Only changes about these resources; absent, changes about any resource or none. */
|
|
85
|
+
filter?: SlotlockWebhookFilter;
|
|
68
86
|
createdAt: Date;
|
|
69
87
|
/** Failed attempts since the last delivery that succeeded. */
|
|
70
88
|
attempts: number;
|
|
@@ -90,10 +108,11 @@ export interface SlotlockWebhookClaim {
|
|
|
90
108
|
export type CreateWebhookEndpointResult = {
|
|
91
109
|
ok: true;
|
|
92
110
|
endpoint: SlotlockWebhookEndpoint;
|
|
93
|
-
secret: string;
|
|
111
|
+
secret: string; /** True when an idempotency key answered what an earlier create made. */
|
|
112
|
+
replayed?: boolean;
|
|
94
113
|
} | {
|
|
95
114
|
ok: false;
|
|
96
|
-
code: 'invalid_webhook_url' | 'invalid_event_types';
|
|
115
|
+
code: 'invalid_webhook_url' | 'invalid_event_types' | 'invalid_webhook_filter' | 'idempotency_conflict' | 'invalid_identity';
|
|
97
116
|
} | {
|
|
98
117
|
ok: false;
|
|
99
118
|
code: 'webhook_limit_reached';
|
|
@@ -136,6 +155,13 @@ export interface SlotlockEventFeed {
|
|
|
136
155
|
tenantRef: string;
|
|
137
156
|
url: string;
|
|
138
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;
|
|
139
165
|
}): Promise<CreateWebhookEndpointResult>;
|
|
140
166
|
listWebhookEndpoints(params: {
|
|
141
167
|
tenantRef: string;
|
package/event-feed.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The event feed: what changed for a tenant, read by position, and the webhook endpoints it is
|
|
2
2
|
// delivered to. Rows are appended by triggers in the changing transaction (ddl.ts,
|
|
3
3
|
// SLOTLOCK_OUTBOX_DDL); webhooks.ts signs and sends.
|
|
4
|
-
import {
|
|
4
|
+
import { slotlockPlanReadRefusal } from './plans.js';
|
|
5
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
5
6
|
/** A transaction on a client, or a savepoint inside the caller's (a tenant's, say). */
|
|
6
7
|
async function inTransaction(sql, callback) {
|
|
7
8
|
return ('begin' in sql ? sql.begin(callback) : sql.savepoint(callback));
|
|
@@ -26,7 +27,14 @@ export const SLOTLOCK_CHANGE_TYPES = Object.freeze([
|
|
|
26
27
|
'coverage.stale',
|
|
27
28
|
'waitlist.offered',
|
|
28
29
|
'waitlist.expired',
|
|
30
|
+
// A write waiting for a person (approval policies): requested, then granted, declined or expired.
|
|
31
|
+
'approval.requested',
|
|
32
|
+
'approval.granted',
|
|
33
|
+
'approval.declined',
|
|
34
|
+
'approval.expired',
|
|
29
35
|
'webhook.disabled',
|
|
36
|
+
// A plan's monthly quota 80% or 100% used (quotas.ts): the metric, the share and the counts.
|
|
37
|
+
'usage.threshold_reached',
|
|
30
38
|
// Configuration changes: ids and states, never a secret or a URL.
|
|
31
39
|
'resource.created',
|
|
32
40
|
'resource.updated',
|
|
@@ -102,9 +110,13 @@ export function slotlockServedChange(type, subject, data) {
|
|
|
102
110
|
/** How long the feed keeps a change, and so how far back a cursor may reach. */
|
|
103
111
|
export const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
|
|
104
112
|
/** Webhook endpoints one tenant may have. */
|
|
105
|
-
export const SLOTLOCK_MAX_WEBHOOK_ENDPOINTS =
|
|
113
|
+
export const SLOTLOCK_MAX_WEBHOOK_ENDPOINTS = 25;
|
|
106
114
|
/** After this long failing, an endpoint is disabled and the feed says so (`webhook.disabled`). */
|
|
107
115
|
export const SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS = 24 * 60 * 60 * 1_000;
|
|
116
|
+
/** Resources one webhook endpoint's filter may name. */
|
|
117
|
+
export const SLOTLOCK_WEBHOOK_FILTER_RESOURCES = 100;
|
|
118
|
+
/** Pools, locations or groups one webhook endpoint's filter may name, of each. */
|
|
119
|
+
export const SLOTLOCK_WEBHOOK_FILTER_REFS = 20;
|
|
108
120
|
function encodePosition(position) {
|
|
109
121
|
return Buffer.from(`2:${position.txid}:${position.id}:${position.at}`).toString('base64url');
|
|
110
122
|
}
|
|
@@ -134,7 +146,7 @@ function toChange(row) {
|
|
|
134
146
|
};
|
|
135
147
|
}
|
|
136
148
|
function toEndpoint(row) {
|
|
137
|
-
|
|
149
|
+
const endpoint = {
|
|
138
150
|
id: row.id,
|
|
139
151
|
url: row.url,
|
|
140
152
|
eventTypes: row.event_types,
|
|
@@ -146,9 +158,85 @@ function toEndpoint(row) {
|
|
|
146
158
|
disabledAt: row.disabled_at,
|
|
147
159
|
disabledReason: row.disabled_reason,
|
|
148
160
|
};
|
|
161
|
+
const filter = {};
|
|
162
|
+
if (row.filter_resource_ids)
|
|
163
|
+
filter.resourceIds = row.filter_resource_ids;
|
|
164
|
+
if (row.filter_pools)
|
|
165
|
+
filter.pools = row.filter_pools;
|
|
166
|
+
if (row.filter_locations)
|
|
167
|
+
filter.locations = row.filter_locations;
|
|
168
|
+
if (row.filter_groups)
|
|
169
|
+
filter.groups = row.filter_groups;
|
|
170
|
+
if (Object.keys(filter).length > 0)
|
|
171
|
+
endpoint.filter = filter;
|
|
172
|
+
return endpoint;
|
|
149
173
|
}
|
|
150
174
|
const ENDPOINT_COLUMNS = (sql) => sql `id, url, event_types, created_at, attempts, failing_since, last_status, last_error,
|
|
151
|
-
disabled_at, disabled_reason
|
|
175
|
+
disabled_at, disabled_reason, filter_resource_ids::text[], filter_pools, filter_locations,
|
|
176
|
+
filter_groups`;
|
|
177
|
+
/** A pool's, location's or group's reference. */
|
|
178
|
+
const GROUPING_REF = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
179
|
+
/**
|
|
180
|
+
* A webhook filter as it is stored, each list without repeats, or null when one is out of bounds.
|
|
181
|
+
* An empty filter, or empty lists, filter nothing.
|
|
182
|
+
*/
|
|
183
|
+
function parseWebhookFilter(filter) {
|
|
184
|
+
const list = (values, max, valid) => {
|
|
185
|
+
if (values === undefined)
|
|
186
|
+
return null;
|
|
187
|
+
if (!Array.isArray(values))
|
|
188
|
+
return false;
|
|
189
|
+
const kept = [...new Set(values)];
|
|
190
|
+
if (kept.length === 0)
|
|
191
|
+
return null;
|
|
192
|
+
if (kept.length > max || !kept.every((value) => typeof value === 'string' && valid(value)))
|
|
193
|
+
return false;
|
|
194
|
+
return kept.sort();
|
|
195
|
+
};
|
|
196
|
+
const resourceIds = list(filter?.resourceIds, SLOTLOCK_WEBHOOK_FILTER_RESOURCES, (value) => UUID.test(value));
|
|
197
|
+
const pools = list(filter?.pools, SLOTLOCK_WEBHOOK_FILTER_REFS, (value) => GROUPING_REF.test(value));
|
|
198
|
+
const locations = list(filter?.locations, SLOTLOCK_WEBHOOK_FILTER_REFS, (value) => GROUPING_REF.test(value));
|
|
199
|
+
const groups = list(filter?.groups, SLOTLOCK_WEBHOOK_FILTER_REFS, (value) => GROUPING_REF.test(value));
|
|
200
|
+
if (resourceIds === false || pools === false || locations === false || groups === false)
|
|
201
|
+
return null;
|
|
202
|
+
return {
|
|
203
|
+
resourceIds: resourceIds?.map((id) => id.toLowerCase()) ?? null,
|
|
204
|
+
pools,
|
|
205
|
+
locations,
|
|
206
|
+
groups,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Whether a change (`change`, a row of slotlock.outbox) passes an endpoint's filter (`due`, a row of
|
|
211
|
+
* slotlock.webhook_endpoints): no filter, or a resource it names, or one in a pool, at a location
|
|
212
|
+
* (or a location in it) or in a group it names, as the resource is now; or a change to a resource
|
|
213
|
+
* that says it was in one (resource.created, resource.deleted, or resource.updated's `before`),
|
|
214
|
+
* so an endpoint also learns of a resource deleted, or moved out of what it follows.
|
|
215
|
+
*/
|
|
216
|
+
const FILTER_MATCH = `(
|
|
217
|
+
(due.filter_resource_ids IS NULL AND due.filter_pools IS NULL
|
|
218
|
+
AND due.filter_locations IS NULL AND due.filter_groups IS NULL)
|
|
219
|
+
OR change.resource_id = ANY (due.filter_resource_ids)
|
|
220
|
+
OR EXISTS (
|
|
221
|
+
SELECT 1 FROM slotlock.resources held
|
|
222
|
+
WHERE held.tenant_ref = change.tenant_ref AND held.id = change.resource_id
|
|
223
|
+
AND (held.pool_ref = ANY (due.filter_pools)
|
|
224
|
+
OR (held.location_ref IS NOT NULL
|
|
225
|
+
AND slotlock.location_path(held.tenant_ref, held.location_ref) && due.filter_locations)
|
|
226
|
+
OR EXISTS (SELECT 1 FROM slotlock.resource_group_members member
|
|
227
|
+
WHERE member.tenant_ref = held.tenant_ref AND member.resource_id = held.id
|
|
228
|
+
AND member.group_ref = ANY (due.filter_groups))))
|
|
229
|
+
OR (change.type LIKE 'resource.%' AND (
|
|
230
|
+
(change.data ->> 'pool') = ANY (due.filter_pools)
|
|
231
|
+
OR (change.data #>> '{before,pool}') = ANY (due.filter_pools)
|
|
232
|
+
OR EXISTS (
|
|
233
|
+
SELECT 1 FROM unnest(ARRAY[change.data ->> 'location', change.data #>> '{before,location}']) was(ref)
|
|
234
|
+
WHERE was.ref IS NOT NULL
|
|
235
|
+
AND (was.ref = ANY (due.filter_locations)
|
|
236
|
+
OR slotlock.location_path(change.tenant_ref, was.ref) && due.filter_locations))
|
|
237
|
+
OR (jsonb_typeof(change.data -> 'groups') = 'array' AND (change.data -> 'groups') ?| due.filter_groups)
|
|
238
|
+
OR (jsonb_typeof(change.data #> '{before,groups}') = 'array'
|
|
239
|
+
AND (change.data #> '{before,groups}') ?| due.filter_groups))))`;
|
|
152
240
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
153
241
|
function isIdentity(value) {
|
|
154
242
|
return typeof value === 'string' && Buffer.byteLength(value) >= 1 && Buffer.byteLength(value) <= 500;
|
|
@@ -268,25 +356,60 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
|
|
|
268
356
|
!types.every((type) => SLOTLOCK_CHANGE_TYPES.includes(type)))) {
|
|
269
357
|
return { ok: false, code: 'invalid_event_types' };
|
|
270
358
|
}
|
|
359
|
+
const filter = parseWebhookFilter(params.filter);
|
|
360
|
+
if (filter === null)
|
|
361
|
+
return { ok: false, code: 'invalid_webhook_filter' };
|
|
362
|
+
const key = params.idempotencyKey;
|
|
363
|
+
if (key !== undefined && !isIdentity(key))
|
|
364
|
+
return { ok: false, code: 'invalid_identity' };
|
|
365
|
+
const payloadHash = key === undefined
|
|
366
|
+
? null
|
|
367
|
+
: createHash('sha256')
|
|
368
|
+
.update(JSON.stringify([params.url, types === null ? null : [...types].sort(), filter]))
|
|
369
|
+
.digest('hex');
|
|
271
370
|
const secret = `whsec_${randomBytes(32).toString('base64')}`;
|
|
272
371
|
return inTransaction(sql, async (tx) => {
|
|
273
372
|
await tx `SELECT pg_advisory_xact_lock(hashtextextended(${`slotlock:webhooks:${params.tenantRef}`}, 0))`;
|
|
373
|
+
if (key !== undefined) {
|
|
374
|
+
// Under the tenant's webhook lock: a retry racing its first finds what the first made.
|
|
375
|
+
const [earlier] = await tx `
|
|
376
|
+
SELECT ${ENDPOINT_COLUMNS(tx)}, secret, payload_hash FROM slotlock.webhook_endpoints
|
|
377
|
+
WHERE tenant_ref = ${params.tenantRef} AND idempotency_key = ${key}`;
|
|
378
|
+
if (earlier) {
|
|
379
|
+
if (earlier.payload_hash !== payloadHash)
|
|
380
|
+
return { ok: false, code: 'idempotency_conflict' };
|
|
381
|
+
return { ok: true, endpoint: toEndpoint(earlier), secret: earlier.secret, replayed: true };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
274
384
|
const [count] = await tx `
|
|
275
|
-
SELECT count(*)::int AS count
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
385
|
+
SELECT count(*)::int AS count,
|
|
386
|
+
(SELECT max_webhook_endpoints FROM slotlock.tenant_plan(${params.tenantRef}::text))
|
|
387
|
+
AS planned
|
|
388
|
+
FROM slotlock.webhook_endpoints
|
|
389
|
+
WHERE tenant_ref = ${params.tenantRef}`.catch((error) => {
|
|
390
|
+
throw slotlockPlanReadRefusal(error);
|
|
391
|
+
});
|
|
392
|
+
// The lower of the server's limit and the tenant's plan (plans.ts), when it has one.
|
|
393
|
+
const limit = Math.min(SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, count?.planned ?? SLOTLOCK_MAX_WEBHOOK_ENDPOINTS);
|
|
394
|
+
if ((count?.count ?? 0) >= limit) {
|
|
395
|
+
return { ok: false, code: 'webhook_limit_reached', limit };
|
|
279
396
|
}
|
|
280
397
|
// From now on: every transaction not yet ended is at or after this position.
|
|
281
398
|
const [row] = await tx `
|
|
282
|
-
INSERT INTO slotlock.webhook_endpoints (
|
|
399
|
+
INSERT INTO slotlock.webhook_endpoints (
|
|
400
|
+
tenant_ref, url, secret, event_types, cursor_txid,
|
|
401
|
+
filter_resource_ids, filter_pools, filter_locations, filter_groups,
|
|
402
|
+
idempotency_key, payload_hash
|
|
403
|
+
)
|
|
283
404
|
VALUES (${params.tenantRef}, ${params.url}, ${secret}, ${types}::text[],
|
|
284
|
-
pg_snapshot_xmin(pg_current_snapshot())
|
|
405
|
+
pg_snapshot_xmin(pg_current_snapshot()),
|
|
406
|
+
${filter.resourceIds}::uuid[], ${filter.pools}::text[], ${filter.locations}::text[],
|
|
407
|
+
${filter.groups}::text[], ${key ?? null}, ${payloadHash})
|
|
285
408
|
RETURNING ${ENDPOINT_COLUMNS(tx)}`;
|
|
286
409
|
if (!row)
|
|
287
410
|
throw new Error('slotlock: webhook endpoint insert returned no row');
|
|
288
411
|
await tx `SELECT slotlock.register_webhook_tenant(${params.tenantRef}::text)`;
|
|
289
|
-
return { ok: true, endpoint: toEndpoint(row), secret };
|
|
412
|
+
return { ok: true, endpoint: toEndpoint(row), secret, replayed: false };
|
|
290
413
|
});
|
|
291
414
|
},
|
|
292
415
|
async listWebhookEndpoints(params) {
|
|
@@ -344,6 +467,32 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
|
|
|
344
467
|
if (!(Number.isSafeInteger(params.batch) && params.batch >= 1 && params.batch <= 100)) {
|
|
345
468
|
throw new RangeError('Slotlock webhook batch must be 1-100 changes');
|
|
346
469
|
}
|
|
470
|
+
// An endpoint that takes only some changes (by type or filter) moves past the ones it does
|
|
471
|
+
// not take, up to the last change that has ended, when none before it is for the endpoint:
|
|
472
|
+
// else its cursor would stay where it was made, keeping the whole feed from being pruned and
|
|
473
|
+
// every claim reading it all again. Not one being delivered (leased), nor one waiting to try
|
|
474
|
+
// again (it has a change for it).
|
|
475
|
+
await sql `
|
|
476
|
+
UPDATE slotlock.webhook_endpoints due
|
|
477
|
+
SET cursor_txid = head.txid, cursor_id = head.id
|
|
478
|
+
FROM (SELECT change.txid, change.id FROM slotlock.outbox change
|
|
479
|
+
WHERE change.tenant_ref = ${params.tenantRef}
|
|
480
|
+
AND change.txid < pg_snapshot_xmin(pg_current_snapshot())
|
|
481
|
+
ORDER BY change.txid DESC, change.id DESC
|
|
482
|
+
LIMIT 1) head
|
|
483
|
+
WHERE due.tenant_ref = ${params.tenantRef}
|
|
484
|
+
AND due.disabled_at IS NULL
|
|
485
|
+
AND (due.lease_until IS NULL OR due.lease_until <= now())
|
|
486
|
+
AND (due.event_types IS NOT NULL OR due.filter_resource_ids IS NOT NULL OR due.filter_pools IS NOT NULL
|
|
487
|
+
OR due.filter_locations IS NOT NULL OR due.filter_groups IS NOT NULL)
|
|
488
|
+
AND (head.txid, head.id) > (due.cursor_txid, due.cursor_id)
|
|
489
|
+
AND NOT EXISTS (
|
|
490
|
+
SELECT 1 FROM slotlock.outbox change
|
|
491
|
+
WHERE change.tenant_ref = due.tenant_ref
|
|
492
|
+
AND (change.txid, change.id) > (due.cursor_txid, due.cursor_id)
|
|
493
|
+
AND (change.txid, change.id) <= (head.txid, head.id)
|
|
494
|
+
AND (due.event_types IS NULL OR change.type = ANY (due.event_types))
|
|
495
|
+
AND ${sql.unsafe(FILTER_MATCH)})`;
|
|
347
496
|
const endpoints = await sql `
|
|
348
497
|
UPDATE slotlock.webhook_endpoints endpoint
|
|
349
498
|
SET lease_until = now() + make_interval(secs => ${params.leaseMs / 1_000}),
|
|
@@ -360,6 +509,7 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
|
|
|
360
509
|
AND change.txid < pg_snapshot_xmin(pg_current_snapshot())
|
|
361
510
|
AND (change.txid, change.id) > (due.cursor_txid, due.cursor_id)
|
|
362
511
|
AND (due.event_types IS NULL OR change.type = ANY (due.event_types))
|
|
512
|
+
AND ${sql.unsafe(FILTER_MATCH)}
|
|
363
513
|
)
|
|
364
514
|
FOR UPDATE SKIP LOCKED
|
|
365
515
|
)
|
|
@@ -368,14 +518,16 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
|
|
|
368
518
|
const claims = [];
|
|
369
519
|
for (const endpoint of endpoints) {
|
|
370
520
|
const rows = await sql `
|
|
371
|
-
SELECT id::text, txid::text, type, resource_id, subject,
|
|
372
|
-
|
|
373
|
-
FROM slotlock.outbox
|
|
374
|
-
WHERE
|
|
375
|
-
AND
|
|
376
|
-
AND
|
|
377
|
-
AND (${endpoint.
|
|
378
|
-
|
|
521
|
+
SELECT change.id::text, change.txid::text, change.type, change.resource_id, change.subject,
|
|
522
|
+
change.data, change.occurred_at, change.actor, change.origin, change.key_id::text
|
|
523
|
+
FROM slotlock.outbox change, slotlock.webhook_endpoints due
|
|
524
|
+
WHERE due.id = ${endpoint.id}::uuid
|
|
525
|
+
AND change.tenant_ref = ${params.tenantRef}
|
|
526
|
+
AND change.txid < pg_snapshot_xmin(pg_current_snapshot())
|
|
527
|
+
AND (change.txid, change.id) > (${endpoint.cursor_txid}::xid8, ${endpoint.cursor_id}::bigint)
|
|
528
|
+
AND (${endpoint.event_types}::text[] IS NULL OR change.type = ANY (${endpoint.event_types}::text[]))
|
|
529
|
+
AND ${sql.unsafe(FILTER_MATCH)}
|
|
530
|
+
ORDER BY change.txid, change.id
|
|
379
531
|
LIMIT ${params.batch}`;
|
|
380
532
|
claims.push({
|
|
381
533
|
endpoint: { id: endpoint.id, url: endpoint.url, secret: endpoint.secret, attempts: endpoint.attempts },
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" preserve="true" />
|
|
2
2
|
export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
|
|
3
3
|
export { type SlotlockBookingRule, type SlotlockBookingRules, type SlotlockBookingSearch, bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
|
|
4
|
+
export { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_FIELDS, SLOTLOCK_RESOURCE_PROFILE_LIMITS, parseSlotlockLocation, parseSlotlockResourceGroup, parseSlotlockResourceProfile, type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceAttributes, type SlotlockResourceKind, type SlotlockResourceProfileChange, type SlotlockResourceProfileInput, } from './resource-profile.js';
|
|
4
5
|
export { type CreateWebhookEndpointResult, type ListChangesResult, SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, slotlockChangeFeedHead, type SlotlockChange, type SlotlockChangeOrigin, type SlotlockChangeType, type SlotlockEventFeed, type SlotlockWebhookClaim, type SlotlockWebhookEndpoint, slotlockCoverageSourceId, } from './event-feed.js';
|
|
5
6
|
export { type SlotlockCloudEvent, type SlotlockWebhookPassResult, type SlotlockWebhookResponse, type SlotlockWebhookScheduler, type SlotlockWebhookSender, createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
|
|
6
7
|
export { type SlotlockMetrics, createSlotlockMetrics } from './metrics.js';
|
|
@@ -11,8 +12,8 @@ export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOC
|
|
|
11
12
|
export type { EmitVAvailabilityOptions, JSCalendarAlert, JSCalendarEvent, JSCalendarParticipant, JSCalendarRecurrenceRule, ParsedVAvailability, } from './interop.js';
|
|
12
13
|
export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
|
|
13
14
|
export type { LocalDateTime, TimeZoneConverter, ZonedCalendarDate, ZonedWallTime, } from './timezone.js';
|
|
14
|
-
export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
|
|
15
|
-
export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockArgumentIssue, SlotlockMcpToolResult, SlotlockTraceContext, } from './agent-server.js';
|
|
15
|
+
export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MAX_ALTERNATIVES, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
|
|
16
|
+
export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockAlternative, SlotlockArgumentIssue, SlotlockMcpToolResult, SlotlockTraceContext, } from './agent-server.js';
|
|
16
17
|
export { createSlotlockNodeServer } from './node-server.js';
|
|
17
18
|
export type { SlotlockNodeListenOptions, SlotlockNodeServer, SlotlockNodeServerAddress, SlotlockNodeServerCloseResult, SlotlockNodeServerErrorPhase, SlotlockNodeServerFetchTarget, SlotlockNodeServerOptions, } from './node-server.js';
|
|
18
19
|
export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
|
|
@@ -22,11 +23,21 @@ export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlock
|
|
|
22
23
|
export type { SlotlockApplicationBackend, SlotlockApplicationInvocationContext, SlotlockApplicationOperation, SlotlockApplicationOperationDefinition, } from './application-api.js';
|
|
23
24
|
export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
|
|
24
25
|
export type { SlotlockMaintenanceResult, SlotlockMaintenanceScheduler, SlotlockMaintenanceSchedulerOptions, } from './maintenance.js';
|
|
25
|
-
export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
|
|
26
|
+
export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, isSlotlockTestTenant, slotlockLiveTenant, SLOTLOCK_TEST_API_KEY_PREFIX, SLOTLOCK_TEST_TENANT_SUFFIX, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
|
|
26
27
|
export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
|
|
27
28
|
export type { SlotlockDashboard, SlotlockDashboardAssistantOptions, SlotlockDashboardOptions, SlotlockDashboardResources, SlotlockDashboardState, } from './dashboard.js';
|
|
28
29
|
export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
|
|
29
30
|
export type { SlotlockAssistantStore } from './assistant.js';
|
|
31
|
+
export { createSlotlockPartnerKeyAuthenticator, createSlotlockPartnerStore, parseSlotlockPartnerContract, slotlockPartnerTenantRef, SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_ID, SLOTLOCK_PARTNER_SCOPE, } from './partners.js';
|
|
32
|
+
export { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from './teams.js';
|
|
33
|
+
export { reportSlotlockOverage, SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT } from './overage.js';
|
|
34
|
+
export type { SlotlockOverageReport } from './overage.js';
|
|
35
|
+
export { createSlotlockOtlpExporter, readSlotlockOtlpConfig, slotlockCallSpan } from './otel.js';
|
|
36
|
+
export type { SlotlockOtlpConfig, SlotlockTraceExporter } from './otel.js';
|
|
37
|
+
export type { SlotlockDashboardTeams } from './teams.js';
|
|
38
|
+
export { closeSlotlockPartnerMonth, priceSlotlockPartnerMonth, slotlockClientLabel, slotlockPartnerMonthChecksum, } from './partner-billing.js';
|
|
39
|
+
export type { SlotlockPartnerAccountMonth, SlotlockPartnerInvoiceLine, SlotlockPartnerMonthClose, } from './partner-billing.js';
|
|
40
|
+
export type { SlotlockPartnerAccount, SlotlockPartnerContract, SlotlockPartnerPlan, SlotlockPartnerPricing, SlotlockPartnerStore, } from './partners.js';
|
|
30
41
|
export type { CreateSlotlockApiKeyInput, SlotlockApiKey, SlotlockApiKeyAuthenticatorOptions, SlotlockApiKeyChange, SlotlockApiKeyIdentity, SlotlockApiKeyScope, SlotlockApiKeyStore, } from './api-keys.js';
|
|
31
42
|
export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
|
|
32
43
|
export type { ICalendarWorkerPool, ICalendarWorkerPoolOptions } from './ical-worker.js';
|
|
@@ -34,4 +45,4 @@ export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emit
|
|
|
34
45
|
export type { ExternalCalendarChange, ExternalCalendarChangeInput, ExternalCalendarStatus, ICalendarEvent, ICalendarExpansionWindow, ICalendarFeedRead, ICalendarSeriesRefusal, ITipMethod, } from './sync.js';
|
|
35
46
|
export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
|
|
36
47
|
export { assertSlotlockSchemaCurrent, calendarEventRollingHorizon, createSlotlockStore, readSlotlockSchemaVersion, SLOTLOCK_AGENT_OWNER_COMMAND_QUOTA, SLOTLOCK_AGENT_OWNER_EVENT_QUOTA, SLOTLOCK_CALENDAR_HORIZON_DAYS, SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS, SLOTLOCK_INTERNAL_EVENT_OWNER_REF, SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE, SLOTLOCK_MATERIALIZATION_WINDOW_DAYS, SLOTLOCK_MAX_EVENT_DURATION_DAYS, SLOTLOCK_TENANT_WRITE_METHODS, type SlotlockHoldTenantOptions, type SlotlockMigrationOptions, type SlotlockSql, type SlotlockStore, type SlotlockStoreOptions, type SlotlockTenantLimits, type SlotlockTenantStore, } from './store.js';
|
|
37
|
-
export type { AcquireHoldResult, BookingRetentionResult, CalendarAttendee, CalendarAttendeeRole, CalendarCoverageAssessment, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventOccurrence, CalendarEventRetentionResult, CalendarEventStatus, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CalendarOrganizer, CalendarParticipationStatus, CalendarRecurrence, CalendarRecurrenceException, CalendarReminder, CalendarTransparency, ConfirmHoldResult, CancelCalendarEventResult, CancelExternalReservationResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockAgendaEntry, SlotlockHold, SlotlockCalendarEvent, SlotlockReservation, SlotlockResource, SlotlockTenantErasure, SlotlockBookingRuleDefaults, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, TrustedICalendarEvent, WeeklyAvailabilityRule, } from './types.js';
|
|
48
|
+
export type { AcquireHoldResult, BookingRetentionResult, CalendarAttendee, CalendarAttendeeRole, CalendarCoverageAssessment, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventOccurrence, CalendarEventRetentionResult, CalendarEventStatus, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CalendarOrganizer, CalendarParticipationStatus, CalendarRecurrence, CalendarRecurrenceException, CalendarReminder, CalendarTransparency, ConfirmHoldResult, CancelCalendarEventResult, CancelExternalReservationResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockAgendaEntry, SlotlockHold, SlotlockCalendarEvent, SlotlockBookingDescription, SlotlockReservation, SlotlockResource, SlotlockTenantErasure, SlotlockBookingRuleDefaults, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, TrustedICalendarEvent, WeeklyAvailabilityRule, } from './types.js';
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
/// <reference types="node" preserve="true" />
|
|
6
6
|
export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
|
|
7
7
|
export { bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
|
|
8
|
+
export { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_FIELDS, SLOTLOCK_RESOURCE_PROFILE_LIMITS, parseSlotlockLocation, parseSlotlockResourceGroup, parseSlotlockResourceProfile, } from './resource-profile.js';
|
|
8
9
|
export { SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, slotlockChangeFeedHead, slotlockCoverageSourceId, } from './event-feed.js';
|
|
9
10
|
export { createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
|
|
10
11
|
export { createSlotlockMetrics } from './metrics.js';
|
|
@@ -12,15 +13,20 @@ export { expandRules } from './rules.js';
|
|
|
12
13
|
export { availabilityRuleProblem, parseAvailabilityRules, rulesToWeeklyHours, SLOTLOCK_MAX_AVAILABILITY_RULES, weeklyHoursToRules, } from './weekly-hours.js';
|
|
13
14
|
export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS, toJSCalendarEvent, } from './interop.js';
|
|
14
15
|
export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
|
|
15
|
-
export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
|
|
16
|
+
export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MAX_ALTERNATIVES, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
|
|
16
17
|
export { createSlotlockNodeServer } from './node-server.js';
|
|
17
18
|
export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
|
|
18
19
|
export { SLOTLOCK_FEED_BUSY_MS, SLOTLOCK_FEED_IDLE_MS, } from './change-watch.js';
|
|
19
20
|
export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlockApplicationOperation, } from './application-api.js';
|
|
20
21
|
export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
|
|
21
|
-
export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
|
|
22
|
+
export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, isSlotlockTestTenant, slotlockLiveTenant, SLOTLOCK_TEST_API_KEY_PREFIX, SLOTLOCK_TEST_TENANT_SUFFIX, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
|
|
22
23
|
export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
|
|
23
24
|
export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
|
|
25
|
+
export { createSlotlockPartnerKeyAuthenticator, createSlotlockPartnerStore, parseSlotlockPartnerContract, slotlockPartnerTenantRef, SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_ID, SLOTLOCK_PARTNER_SCOPE, } from './partners.js';
|
|
26
|
+
export { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from './teams.js';
|
|
27
|
+
export { reportSlotlockOverage, SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT } from './overage.js';
|
|
28
|
+
export { createSlotlockOtlpExporter, readSlotlockOtlpConfig, slotlockCallSpan } from './otel.js';
|
|
29
|
+
export { closeSlotlockPartnerMonth, priceSlotlockPartnerMonth, slotlockClientLabel, slotlockPartnerMonthChecksum, } from './partner-billing.js';
|
|
24
30
|
export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
|
|
25
31
|
export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emitITipCalendar, expandCalendarEventOccurrences, normalizeCalendarEventContent, normalizeExternalCalendarChange, parseICalendarChanges, parseICalendarFeed, parseTrustedICalendarEvents, recurrenceRuleProblem, storedCalendarEventProblem, } from './sync.js';
|
|
26
32
|
export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
|
package/maintenance.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface SlotlockMaintenanceResult {
|
|
|
24
24
|
noShows: number;
|
|
25
25
|
/** Waitlist entries whose window came free, each now holding it (`waitlist.offered`). */
|
|
26
26
|
waitlistOffers: number;
|
|
27
|
+
/** Approvals no one decided in time, their held time freed (`approval.expired`). */
|
|
28
|
+
approvalsExpired: number;
|
|
27
29
|
/** More was left than one pass does: run another. */
|
|
28
30
|
capped: boolean;
|
|
29
31
|
/**
|
|
@@ -41,6 +43,12 @@ export declare function maintainSlotlockTenant(store: SlotlockStore, tenantRef:
|
|
|
41
43
|
batches?: number;
|
|
42
44
|
signal?: AbortSignal;
|
|
43
45
|
followZoneRules?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Roll the tenant's committed bookings and holds up into its usage first (usage.ts), before
|
|
48
|
+
* pruning can delete feed changes it has not counted. For a deployment that meters
|
|
49
|
+
* (SLOTLOCK_USAGE=on).
|
|
50
|
+
*/
|
|
51
|
+
usage?: boolean;
|
|
44
52
|
}): Promise<SlotlockMaintenanceResult>;
|
|
45
53
|
/**
|
|
46
54
|
* Run `work` holding the tenant's maintenance lock on a connection of its own, or return
|
|
@@ -73,6 +81,8 @@ export interface SlotlockMaintenanceSchedulerOptions {
|
|
|
73
81
|
maxTenants?: number;
|
|
74
82
|
/** Batches of each task per pass (maintainSlotlockTenant). */
|
|
75
83
|
batches?: number;
|
|
84
|
+
/** Roll each tenant's bookings and holds up into its usage, first (maintainSlotlockTenant `usage`). */
|
|
85
|
+
usage?: boolean;
|
|
76
86
|
/** A pass that ran: counts only, never the tenant. Not awaited; what it throws is ignored. */
|
|
77
87
|
onPass?: (result: SlotlockMaintenanceResult) => void;
|
|
78
88
|
/**
|
package/maintenance.js
CHANGED
|
@@ -11,6 +11,14 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
|
11
11
|
throw new RangeError('batches must be a whole number from 1 to 100');
|
|
12
12
|
}
|
|
13
13
|
const withTenant = (callback) => store.withTenant(tenantRef, callback, { origin: 'maintenance' });
|
|
14
|
+
if (options.usage) {
|
|
15
|
+
for (let batch = 0; batch < batches; batch++) {
|
|
16
|
+
options.signal?.throwIfAborted();
|
|
17
|
+
const read = await withTenant((tenant) => tenant.rollUpUsage({ tenantRef, limit: USAGE_ROLL_UP_BATCH }));
|
|
18
|
+
if (read < USAGE_ROLL_UP_BATCH)
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
14
22
|
const window = calendarEventRollingHorizon();
|
|
15
23
|
let extended = 0;
|
|
16
24
|
let conflicts = 0;
|
|
@@ -66,7 +74,9 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
|
66
74
|
pruned += await withTenant((tenant) => tenant.pruneAssistantConversations({ tenantRef }));
|
|
67
75
|
const zones = await withTenant((tenant) => tenant.followZoneRules({ tenantRef, move: options.followZoneRules === true }));
|
|
68
76
|
const staleSources = await withTenant((tenant) => tenant.reportStaleCoverage({ tenantRef }));
|
|
69
|
-
//
|
|
77
|
+
// Approvals past their wait, and no-shows, first: the time they free is offered to the waitlist
|
|
78
|
+
// in the same pass.
|
|
79
|
+
const approvals = await withTenant((tenant) => tenant.expireApprovals({ tenantRef, limit: 100 }));
|
|
70
80
|
const noShows = await withTenant((tenant) => tenant.releaseNoShows({ tenantRef }));
|
|
71
81
|
// The whole waitlist in join order, a page per transaction, so a later free entry is offered
|
|
72
82
|
// however many wait before it.
|
|
@@ -92,7 +102,8 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
|
92
102
|
staleSources,
|
|
93
103
|
noShows,
|
|
94
104
|
waitlistOffers,
|
|
95
|
-
|
|
105
|
+
approvalsExpired: approvals.expired,
|
|
106
|
+
capped: rollCapped || eventsLeft || bookingsLeft || changesLeft || approvals.expired === 100,
|
|
96
107
|
rollPending: rollCapped && after !== null && (after.rollRetryAt ?? null) === null,
|
|
97
108
|
};
|
|
98
109
|
}
|
|
@@ -102,6 +113,8 @@ const WAITLIST_PAGES = Math.ceil(SLOTLOCK_MAX_WAITLIST_PER_TENANT / 50);
|
|
|
102
113
|
const SWEEP_INTERVAL_MS = 15 * 60_000;
|
|
103
114
|
/** Feed changes past the retention deleted per batch. */
|
|
104
115
|
const CHANGE_PRUNE_BATCH = 1_000;
|
|
116
|
+
/** Feed changes rolled up into usage per batch. */
|
|
117
|
+
const USAGE_ROLL_UP_BATCH = 5_000;
|
|
105
118
|
/** The advisory lock key for one tenant's maintenance (64-bit, from the tenant reference). */
|
|
106
119
|
const LOCK_NAMESPACE = 'slotlock.maintenance:';
|
|
107
120
|
/**
|
|
@@ -169,6 +182,7 @@ export function startSlotlockMaintenance(options) {
|
|
|
169
182
|
try {
|
|
170
183
|
const result = await withSlotlockMaintenanceLock(options.sql, tenantRef, () => maintainSlotlockTenant(options.store, tenantRef, {
|
|
171
184
|
...(options.batches !== undefined ? { batches: options.batches } : {}),
|
|
185
|
+
...(options.usage ? { usage: true } : {}),
|
|
172
186
|
signal: controller.signal,
|
|
173
187
|
}));
|
|
174
188
|
recordPass(tenantRef, Date.now());
|
package/metrics.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { SlotlockAgentServerEvent } from './agent-server.js';
|
|
|
2
2
|
import type { SlotlockMaintenanceResult } from './maintenance.js';
|
|
3
3
|
import type { SlotlockRefusalCounts } from './rate-limit.js';
|
|
4
4
|
import type { SlotlockWebhookPassResult } from './webhooks.js';
|
|
5
|
+
/** A metric's labels, by name. */
|
|
6
|
+
export type Labels = Readonly<Record<string, string>>;
|
|
5
7
|
export interface SlotlockMetrics {
|
|
6
8
|
/** Count what the agent server reports (its `onEvent`). */
|
|
7
9
|
event(event: SlotlockAgentServerEvent): void;
|
|
@@ -13,8 +15,47 @@ export interface SlotlockMetrics {
|
|
|
13
15
|
webhooks(result: SlotlockWebhookPassResult): void;
|
|
14
16
|
/** Count a scrape refused for its credential. */
|
|
15
17
|
refusedScrape(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Count an authenticated request by the integrator its `Slotlock-Client` field names (RFC 0003
|
|
20
|
+
* P5): a name from the deployment's known list, `other` or `none` (slotlockClientLabel).
|
|
21
|
+
*/
|
|
22
|
+
client(name: string): void;
|
|
16
23
|
/** Everything counted so far, as a scrape answers it. */
|
|
17
24
|
render(): string;
|
|
25
|
+
/** Everything counted so far, as data: for an OpenTelemetry metrics export (otel.ts). */
|
|
26
|
+
collect(): SlotlockMetricsSnapshot;
|
|
27
|
+
}
|
|
28
|
+
/** The counters and histograms of one server since it started (cumulative). */
|
|
29
|
+
export interface SlotlockMetricsSnapshot {
|
|
30
|
+
startedAtMs: number;
|
|
31
|
+
counters: {
|
|
32
|
+
name: string;
|
|
33
|
+
help: string;
|
|
34
|
+
points: {
|
|
35
|
+
labels: Labels;
|
|
36
|
+
value: number;
|
|
37
|
+
}[];
|
|
38
|
+
}[];
|
|
39
|
+
histograms: {
|
|
40
|
+
name: string;
|
|
41
|
+
help: string;
|
|
42
|
+
/** Upper bounds, in seconds; each point's buckets count what fell at or under each one. */
|
|
43
|
+
bounds: readonly number[];
|
|
44
|
+
points: {
|
|
45
|
+
labels: Labels;
|
|
46
|
+
cumulative: number[];
|
|
47
|
+
sum: number;
|
|
48
|
+
count: number;
|
|
49
|
+
}[];
|
|
50
|
+
}[];
|
|
51
|
+
gauges: {
|
|
52
|
+
name: string;
|
|
53
|
+
help: string;
|
|
54
|
+
points: {
|
|
55
|
+
labels: Labels;
|
|
56
|
+
value: number;
|
|
57
|
+
}[];
|
|
58
|
+
}[];
|
|
18
59
|
}
|
|
19
60
|
/** Counters for one server process, reset when it restarts (`slotlock_start_time_seconds`). */
|
|
20
61
|
export declare function createSlotlockMetrics(info: {
|