slotlock 0.2.0 → 0.5.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 +582 -0
- package/CONTRIBUTING.md +35 -8
- package/GOVERNANCE.md +4 -2
- package/MAINTAINERS.md +3 -2
- package/NOTICE +4 -4
- package/README.md +498 -65
- package/SECURITY.md +31 -2
- package/SPEC.md +142 -15
- package/agent-server.d.ts +62 -10
- package/agent-server.js +577 -105
- package/agent-store-backend.d.ts +12 -0
- package/agent-store-backend.js +1115 -55
- package/api-keys.d.ts +11 -3
- package/api-keys.js +29 -6
- package/application-api.d.ts +35 -1
- package/application-api.js +683 -12
- package/booking-rules.d.ts +84 -0
- package/booking-rules.js +149 -0
- package/config.d.ts +112 -0
- package/config.js +225 -0
- package/dashboard.d.ts +67 -5
- package/dashboard.js +1012 -140
- package/ddl.d.ts +45 -3
- package/ddl.js +1124 -13
- package/engine.d.ts +5 -0
- package/engine.js +23 -6
- package/event-feed.d.ts +178 -0
- package/event-feed.js +435 -0
- package/event-schema.d.ts +59 -0
- package/event-schema.js +85 -0
- package/ical-worker-thread.js +4 -2
- package/ical-worker.d.ts +8 -5
- package/ical-worker.js +9 -2
- package/index.d.ts +12 -6
- package/index.js +9 -4
- package/interop.d.ts +117 -0
- package/interop.js +356 -0
- package/maintenance.d.ts +27 -5
- package/maintenance.js +92 -28
- package/mcp-app.d.ts +7 -1
- package/mcp-app.js +259 -40
- package/metrics.d.ts +24 -0
- package/metrics.js +167 -0
- package/node-server.d.ts +9 -1
- package/node-server.js +7 -2
- package/openapi.json +10970 -0
- package/package.json +3 -2
- package/rate-limit.d.ts +80 -0
- package/rate-limit.js +741 -0
- package/rest-api.d.ts +22 -0
- package/rest-api.js +166 -11
- package/rules.js +6 -3
- package/self-host.d.ts +45 -8
- package/self-host.js +616 -46
- package/slotlock-manifest.schema.json +7 -1
- package/slotlock.manifest.json +8 -2
- package/store.d.ts +647 -28
- package/store.js +2982 -327
- package/sync.d.ts +43 -2
- package/sync.js +1515 -323
- package/timezone.d.ts +58 -3
- package/timezone.js +249 -20
- package/types.d.ts +300 -9
- package/types.js +0 -2
- package/webhooks.d.ts +91 -0
- package/webhooks.js +300 -0
package/api-keys.d.ts
CHANGED
|
@@ -117,7 +117,10 @@ export declare function createSlotlockApiKeyStore(sql: SlotlockSql): SlotlockApi
|
|
|
117
117
|
export interface SlotlockApiKeyAuthenticatorOptions {
|
|
118
118
|
/** Key lookups that may run at once, default 2: at most this many connections serve them. */
|
|
119
119
|
maxConcurrentLookups?: number;
|
|
120
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Lookups that may wait for one, default 256, and as many again for keys that recently
|
|
122
|
+
* authenticated. A request beyond that is refused as busy at once.
|
|
123
|
+
*/
|
|
121
124
|
maxWaitingLookups?: number;
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
@@ -126,7 +129,12 @@ export interface SlotlockApiKeyAuthenticatorOptions {
|
|
|
126
129
|
* `authorize`. A value that is not a well-formed key is anonymous without a lookup.
|
|
127
130
|
*
|
|
128
131
|
* Anyone can make well-formed keys, so lookups are bounded: a flood of made-up keys waits for at
|
|
129
|
-
* most `maxConcurrentLookups` connections and
|
|
130
|
-
*
|
|
132
|
+
* most `maxConcurrentLookups` connections, and past `maxWaitingLookups` a request is refused with a
|
|
133
|
+
* `SlotlockAgentOperationError` (`server_busy`, 503) rather than answered anonymous, which would
|
|
134
|
+
* tell a client holding a valid key that its key is bad. A key that authenticated in the last 15
|
|
135
|
+
* minutes waits in a queue of its own that is served first, so made-up keys, which never
|
|
136
|
+
* authenticate, cannot keep it waiting. Every request is still looked up, so a revoked key stops
|
|
137
|
+
* at once. Rate-limit a public server in front of it as well (`slotlock serve` does, per client
|
|
138
|
+
* address, before any lookup).
|
|
131
139
|
*/
|
|
132
140
|
export declare function createSlotlockApiKeyAuthenticator(keys: Pick<SlotlockApiKeyStore, 'authenticate'>, options?: SlotlockApiKeyAuthenticatorOptions): (request: Request) => Promise<SlotlockAgentPrincipal | null>;
|
package/api-keys.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// cannot reach another tenant's keys, read a digest, or bring back a revoked, rotated-out or erased
|
|
9
9
|
// key.
|
|
10
10
|
import { createHash, randomBytes } from 'node:crypto';
|
|
11
|
+
import { SlotlockAgentOperationError, } from './agent-server.js';
|
|
11
12
|
import { SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_RETAINED_LIMIT } from './ddl.js';
|
|
12
13
|
export { SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_RETAINED_LIMIT };
|
|
13
14
|
/**
|
|
@@ -41,6 +42,10 @@ const DAY_MS = 86_400_000;
|
|
|
41
42
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
42
43
|
const DEFAULT_MAX_CONCURRENT_LOOKUPS = 2;
|
|
43
44
|
const DEFAULT_MAX_WAITING_LOOKUPS = 256;
|
|
45
|
+
/** How long a key that authenticated keeps its place ahead of unknown keys in the lookup queue. */
|
|
46
|
+
const RECENTLY_VALID_MS = 15 * 60_000;
|
|
47
|
+
/** Keys the authenticator remembers as recently valid, by digest; past this the oldest go first. */
|
|
48
|
+
const MAX_RECENTLY_VALID = 10_000;
|
|
44
49
|
function invalid(code, message) {
|
|
45
50
|
return Object.assign(new Error(message), { code });
|
|
46
51
|
}
|
|
@@ -230,14 +235,21 @@ export function createSlotlockApiKeyStore(sql) {
|
|
|
230
235
|
},
|
|
231
236
|
};
|
|
232
237
|
}
|
|
238
|
+
/** What the authenticator throws when its lookup queue is full: retry later, the key may be fine. */
|
|
239
|
+
const LOOKUP_BUSY = 'server_busy';
|
|
233
240
|
/**
|
|
234
241
|
* Bearer authentication by API key: a request carrying an active key is that key's principal,
|
|
235
242
|
* `api_key:<id>` in the key's own tenant with its scopes, which the agent server enforces before
|
|
236
243
|
* `authorize`. A value that is not a well-formed key is anonymous without a lookup.
|
|
237
244
|
*
|
|
238
245
|
* Anyone can make well-formed keys, so lookups are bounded: a flood of made-up keys waits for at
|
|
239
|
-
* most `maxConcurrentLookups` connections and
|
|
240
|
-
*
|
|
246
|
+
* most `maxConcurrentLookups` connections, and past `maxWaitingLookups` a request is refused with a
|
|
247
|
+
* `SlotlockAgentOperationError` (`server_busy`, 503) rather than answered anonymous, which would
|
|
248
|
+
* tell a client holding a valid key that its key is bad. A key that authenticated in the last 15
|
|
249
|
+
* minutes waits in a queue of its own that is served first, so made-up keys, which never
|
|
250
|
+
* authenticate, cannot keep it waiting. Every request is still looked up, so a revoked key stops
|
|
251
|
+
* at once. Rate-limit a public server in front of it as well (`slotlock serve` does, per client
|
|
252
|
+
* address, before any lookup).
|
|
241
253
|
*/
|
|
242
254
|
export function createSlotlockApiKeyAuthenticator(keys, options = {}) {
|
|
243
255
|
const maxActive = options.maxConcurrentLookups ?? DEFAULT_MAX_CONCURRENT_LOOKUPS;
|
|
@@ -250,26 +262,37 @@ export function createSlotlockApiKeyAuthenticator(keys, options = {}) {
|
|
|
250
262
|
}
|
|
251
263
|
let active = 0;
|
|
252
264
|
const waiting = [];
|
|
265
|
+
const waitingKnown = [];
|
|
266
|
+
/** Digests of keys that recently authenticated, oldest first, with when they stop counting. */
|
|
267
|
+
const recentlyValid = new Map();
|
|
253
268
|
return async (request) => {
|
|
254
269
|
const presented = slotlockBearerCredential(request);
|
|
255
270
|
if (presented === undefined || !isSlotlockApiKey(presented))
|
|
256
271
|
return null;
|
|
272
|
+
const digest = slotlockApiKeyDigest(presented).toString('hex');
|
|
273
|
+
const known = (recentlyValid.get(digest) ?? 0) > Date.now();
|
|
274
|
+
const queue = known ? waitingKnown : waiting;
|
|
257
275
|
if (active < maxActive) {
|
|
258
276
|
active += 1;
|
|
259
277
|
}
|
|
260
|
-
else if (
|
|
278
|
+
else if (queue.length < maxWaiting) {
|
|
261
279
|
// A finishing lookup hands its slot straight to the next waiter, so `active` is unchanged.
|
|
262
|
-
await new Promise((resolve) =>
|
|
280
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
263
281
|
}
|
|
264
282
|
else {
|
|
265
|
-
|
|
283
|
+
throw new SlotlockAgentOperationError(LOOKUP_BUSY, 503);
|
|
266
284
|
}
|
|
267
285
|
try {
|
|
268
286
|
if (request.signal.aborted)
|
|
269
287
|
return null;
|
|
270
288
|
const identity = await keys.authenticate(presented);
|
|
289
|
+
recentlyValid.delete(digest);
|
|
271
290
|
if (!identity)
|
|
272
291
|
return null;
|
|
292
|
+
recentlyValid.set(digest, Date.now() + RECENTLY_VALID_MS);
|
|
293
|
+
if (recentlyValid.size > MAX_RECENTLY_VALID) {
|
|
294
|
+
recentlyValid.delete(recentlyValid.keys().next().value);
|
|
295
|
+
}
|
|
273
296
|
return {
|
|
274
297
|
subject: `api_key:${identity.id}`,
|
|
275
298
|
tenantRef: identity.tenantRef,
|
|
@@ -277,7 +300,7 @@ export function createSlotlockApiKeyAuthenticator(keys, options = {}) {
|
|
|
277
300
|
};
|
|
278
301
|
}
|
|
279
302
|
finally {
|
|
280
|
-
const next = waiting.shift();
|
|
303
|
+
const next = waitingKnown.shift() ?? waiting.shift();
|
|
281
304
|
if (next)
|
|
282
305
|
next();
|
|
283
306
|
else
|
package/application-api.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { SlotlockAgentPrincipal } from './agent-server.js';
|
|
3
3
|
import type { SlotlockTraceContext } from './mcp-modern.js';
|
|
4
|
-
export type SlotlockApplicationOperation = 'slotlock_create_resource' | 'slotlock_get_resource' | 'slotlock_set_resource_availability' | 'slotlock_set_tenant_availability' | 'slotlock_acquire_hold' | 'slotlock_confirm_hold' | 'slotlock_release_hold' | 'slotlock_put_reservation' | 'slotlock_cancel_reservation' | 'slotlock_record_coverage' | 'slotlock_run_maintenance';
|
|
4
|
+
export type SlotlockApplicationOperation = 'slotlock_create_resource' | 'slotlock_get_resource' | 'slotlock_archive_resource' | 'slotlock_restore_resource' | 'slotlock_delete_resource' | 'slotlock_list_busy' | 'slotlock_set_resource_availability' | 'slotlock_set_tenant_availability' | 'slotlock_export_resource_availability' | 'slotlock_import_resource_availability' | 'slotlock_set_resource_booking_rules' | 'slotlock_set_resource_pool' | 'slotlock_list_pools' | 'slotlock_get_resource_history' | 'slotlock_get_coverage' | 'slotlock_record_coverage_failure' | 'slotlock_delete_coverage' | 'slotlock_get_reservation_history' | 'slotlock_get_tenant_event_history' | 'slotlock_acquire_hold' | 'slotlock_get_hold' | 'slotlock_list_holds' | 'slotlock_confirm_hold' | 'slotlock_release_hold' | 'slotlock_get_reservation' | 'slotlock_list_reservations' | 'slotlock_put_reservation' | 'slotlock_cancel_reservation' | 'slotlock_check_in_reservation' | 'slotlock_join_waitlist' | 'slotlock_list_waitlist' | 'slotlock_leave_waitlist' | 'slotlock_record_coverage' | 'slotlock_list_tenant_events' | 'slotlock_get_tenant_event' | 'slotlock_export_tenant_event' | 'slotlock_cancel_tenant_event' | 'slotlock_get_tenant' | 'slotlock_erase_tenant' | 'slotlock_run_maintenance' | 'slotlock_list_changes' | 'slotlock_create_webhook' | 'slotlock_list_webhooks' | 'slotlock_delete_webhook' | 'slotlock_enable_webhook';
|
|
5
5
|
/** The scope an application operation needs, whichever operation it is. */
|
|
6
6
|
export declare const SLOTLOCK_APPLICATION_SCOPE = "admin";
|
|
7
7
|
export interface SlotlockApplicationInvocationContext {
|
|
@@ -20,15 +20,49 @@ type Handler = (context: SlotlockApplicationInvocationContext, input: Record<str
|
|
|
20
20
|
export interface SlotlockApplicationBackend {
|
|
21
21
|
createResource: Handler;
|
|
22
22
|
getResource: Handler;
|
|
23
|
+
archiveResource: Handler;
|
|
24
|
+
restoreResource: Handler;
|
|
25
|
+
deleteResource: Handler;
|
|
26
|
+
listBusy: Handler;
|
|
23
27
|
setResourceAvailability: Handler;
|
|
24
28
|
setTenantAvailability: Handler;
|
|
29
|
+
exportResourceAvailability: Handler;
|
|
30
|
+
importResourceAvailability: Handler;
|
|
31
|
+
setResourceBookingRules: Handler;
|
|
32
|
+
setResourcePool: Handler;
|
|
33
|
+
listPools: Handler;
|
|
34
|
+
getResourceHistory: Handler;
|
|
35
|
+
getCoverage: Handler;
|
|
36
|
+
recordCoverageFailure: Handler;
|
|
37
|
+
deleteCoverage: Handler;
|
|
38
|
+
getReservationHistory: Handler;
|
|
39
|
+
getTenantEventHistory: Handler;
|
|
25
40
|
acquireHold: Handler;
|
|
41
|
+
getHold: Handler;
|
|
42
|
+
listHolds: Handler;
|
|
26
43
|
confirmHold: Handler;
|
|
27
44
|
releaseHold: Handler;
|
|
45
|
+
getReservation: Handler;
|
|
46
|
+
listReservations: Handler;
|
|
28
47
|
putReservation: Handler;
|
|
29
48
|
cancelReservation: Handler;
|
|
49
|
+
checkInReservation: Handler;
|
|
50
|
+
joinWaitlist: Handler;
|
|
51
|
+
listWaitlist: Handler;
|
|
52
|
+
leaveWaitlist: Handler;
|
|
30
53
|
recordCoverage: Handler;
|
|
54
|
+
listTenantEvents: Handler;
|
|
55
|
+
getTenantEvent: Handler;
|
|
56
|
+
exportTenantEvent: Handler;
|
|
57
|
+
cancelTenantEvent: Handler;
|
|
58
|
+
getTenant: Handler;
|
|
59
|
+
eraseTenant: Handler;
|
|
31
60
|
runMaintenance: Handler;
|
|
61
|
+
listChanges: Handler;
|
|
62
|
+
createWebhook: Handler;
|
|
63
|
+
listWebhooks: Handler;
|
|
64
|
+
deleteWebhook: Handler;
|
|
65
|
+
enableWebhook: Handler;
|
|
32
66
|
}
|
|
33
67
|
export interface SlotlockApplicationOperationDefinition {
|
|
34
68
|
name: SlotlockApplicationOperation;
|