slotlock 0.3.0 → 0.5.1
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 +265 -0
- package/README.md +226 -17
- package/agent-server.d.ts +51 -8
- package/agent-server.js +478 -36
- package/agent-store-backend.d.ts +6 -0
- package/agent-store-backend.js +771 -30
- package/application-api.d.ts +22 -1
- package/application-api.js +412 -3
- package/booking-rules.d.ts +84 -0
- package/booking-rules.js +149 -0
- package/config.d.ts +20 -1
- package/config.js +33 -0
- package/dashboard.d.ts +61 -5
- package/dashboard.js +995 -139
- package/ddl.d.ts +5 -5
- package/ddl.js +591 -5
- package/engine.d.ts +5 -0
- package/engine.js +7 -4
- package/event-feed.d.ts +178 -0
- package/event-feed.js +447 -0
- package/index.d.ts +8 -2
- package/index.js +6 -1
- package/interop.d.ts +117 -0
- package/interop.js +356 -0
- package/maintenance.d.ts +14 -2
- package/maintenance.js +35 -3
- 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/openapi.json +8346 -3400
- package/package.json +1 -1
- package/rate-limit.d.ts +10 -1
- package/rate-limit.js +25 -2
- package/rest-api.d.ts +22 -0
- package/rest-api.js +160 -15
- package/self-host.d.ts +26 -3
- package/self-host.js +316 -19
- package/slotlock-manifest.schema.json +1 -0
- package/slotlock.manifest.json +2 -1
- package/store.d.ts +317 -8
- package/store.js +1005 -89
- package/types.d.ts +87 -0
- package/types.js +0 -2
- package/webhooks.d.ts +91 -0
- package/webhooks.js +300 -0
package/booking-rules.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { expandRules } from './rules.js';
|
|
2
|
+
import { zonedOffsetMinutes } from './timezone.js';
|
|
3
|
+
const MINUTE_MS = 60_000;
|
|
4
|
+
const DAY_MINUTES = 24 * 60;
|
|
5
|
+
/** Each numeric rule's least and greatest value. */
|
|
6
|
+
export const SLOTLOCK_BOOKING_RULE_LIMITS = Object.freeze({
|
|
7
|
+
minNoticeMinutes: [0, 525_600],
|
|
8
|
+
maxAdvanceDays: [1, 3_660],
|
|
9
|
+
slotStepMinutes: [1, DAY_MINUTES],
|
|
10
|
+
turnaroundMinutes: [0, DAY_MINUTES],
|
|
11
|
+
checkInMinutes: [1, DAY_MINUTES],
|
|
12
|
+
});
|
|
13
|
+
const LIMITS = SLOTLOCK_BOOKING_RULE_LIMITS;
|
|
14
|
+
/** Booking rules from untrusted input, or a TypeError naming the field that is wrong. */
|
|
15
|
+
export function parseBookingRules(value) {
|
|
16
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
17
|
+
throw new TypeError('Slotlock booking rules must be an object');
|
|
18
|
+
}
|
|
19
|
+
const input = value;
|
|
20
|
+
const rules = {};
|
|
21
|
+
for (const key of Object.keys(input)) {
|
|
22
|
+
if (!(key in LIMITS) && key !== 'enforceHours' && key !== 'requireCertain') {
|
|
23
|
+
throw new TypeError(`Slotlock booking rules have no ${key}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const [key, [min, max]] of Object.entries(LIMITS)) {
|
|
27
|
+
const field = input[key];
|
|
28
|
+
if (field === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
if (typeof field !== 'number' || !Number.isSafeInteger(field) || field < min || field > max) {
|
|
31
|
+
throw new TypeError(`Slotlock booking rule ${key} must be a whole ${min}-${max}`);
|
|
32
|
+
}
|
|
33
|
+
rules[key] = field;
|
|
34
|
+
}
|
|
35
|
+
if (input.enforceHours !== undefined) {
|
|
36
|
+
if (typeof input.enforceHours !== 'boolean') {
|
|
37
|
+
throw new TypeError('Slotlock booking rule enforceHours must be true or false');
|
|
38
|
+
}
|
|
39
|
+
rules.enforceHours = input.enforceHours;
|
|
40
|
+
}
|
|
41
|
+
if (input.requireCertain !== undefined) {
|
|
42
|
+
if (typeof input.requireCertain !== 'boolean') {
|
|
43
|
+
throw new TypeError('Slotlock booking rule requireCertain must be true or false');
|
|
44
|
+
}
|
|
45
|
+
rules.requireCertain = input.requireCertain;
|
|
46
|
+
}
|
|
47
|
+
return rules;
|
|
48
|
+
}
|
|
49
|
+
/** The trailing buffer a booking made under the rules keeps: the larger of its own and the rule's. */
|
|
50
|
+
export function bookingBufferAfterMs(rules, ownMs = 0) {
|
|
51
|
+
return Math.max(ownMs, (rules?.turnaroundMinutes ?? 0) * MINUTE_MS);
|
|
52
|
+
}
|
|
53
|
+
/** Minutes past local midnight in `timezone`, with the fraction. */
|
|
54
|
+
function localMinutes(instant, timezone) {
|
|
55
|
+
const local = instant.getTime() + zonedOffsetMinutes(instant, timezone) * MINUTE_MS;
|
|
56
|
+
return (((local / MINUTE_MS) % DAY_MINUTES) + DAY_MINUTES) % DAY_MINUTES;
|
|
57
|
+
}
|
|
58
|
+
function onStep(instant, stepMinutes, timezone) {
|
|
59
|
+
const minutes = localMinutes(instant, timezone);
|
|
60
|
+
return Number.isInteger(minutes) && minutes % stepMinutes === 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The first instant at or after `from` that starts on a step in `timezone`. A step that does not
|
|
64
|
+
* divide the day starts again at local midnight. Across a clock change it moves forward until the
|
|
65
|
+
* local time is on a step.
|
|
66
|
+
*/
|
|
67
|
+
export function nextStepStart(from, stepMinutes, timezone) {
|
|
68
|
+
let candidate = from;
|
|
69
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
70
|
+
if (onStep(candidate, stepMinutes, timezone))
|
|
71
|
+
return candidate;
|
|
72
|
+
const minutes = localMinutes(candidate, timezone);
|
|
73
|
+
const next = Math.min(Math.ceil(minutes / stepMinutes + 1e-9) * stepMinutes, DAY_MINUTES);
|
|
74
|
+
const ahead = Math.max(next - minutes, 1 / 60_000);
|
|
75
|
+
candidate = new Date(Math.ceil(candidate.getTime() + ahead * MINUTE_MS));
|
|
76
|
+
}
|
|
77
|
+
return candidate;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The rule a booking of `interval` made at `now` breaks, or null. `hours` are the resource's
|
|
81
|
+
* bookable hours (its own, or the default), read only when the rules enforce them.
|
|
82
|
+
*/
|
|
83
|
+
export function bookingRuleBroken(params) {
|
|
84
|
+
const { rules, interval, now } = params;
|
|
85
|
+
if (!rules)
|
|
86
|
+
return null;
|
|
87
|
+
const start = interval.start.getTime();
|
|
88
|
+
if (rules.minNoticeMinutes !== undefined && start < now.getTime() + rules.minNoticeMinutes * MINUTE_MS) {
|
|
89
|
+
return 'min_notice';
|
|
90
|
+
}
|
|
91
|
+
if (rules.maxAdvanceDays !== undefined &&
|
|
92
|
+
start > now.getTime() + rules.maxAdvanceDays * DAY_MINUTES * MINUTE_MS) {
|
|
93
|
+
return 'max_advance';
|
|
94
|
+
}
|
|
95
|
+
if (rules.slotStepMinutes !== undefined && !onStep(interval.start, rules.slotStepMinutes, params.timezone)) {
|
|
96
|
+
return 'slot_step';
|
|
97
|
+
}
|
|
98
|
+
if (rules.enforceHours === true) {
|
|
99
|
+
const open = expandRules([...params.hours], interval, params.timezone);
|
|
100
|
+
let covered = interval.start.getTime();
|
|
101
|
+
for (const window of open.sort((a, b) => a.start.getTime() - b.start.getTime())) {
|
|
102
|
+
if (window.start.getTime() > covered)
|
|
103
|
+
break;
|
|
104
|
+
covered = Math.max(covered, window.end.getTime());
|
|
105
|
+
if (covered >= interval.end.getTime())
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (covered < interval.end.getTime())
|
|
109
|
+
return 'outside_hours';
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* How a search honours the rules: the part of `window` a booking made at `now` may start in, the
|
|
115
|
+
* trailing buffer each slot keeps, and how a candidate start moves to a step. `null` when no start
|
|
116
|
+
* in the window is allowed.
|
|
117
|
+
*/
|
|
118
|
+
export function bookingSearch(params) {
|
|
119
|
+
const { rules, window, now } = params;
|
|
120
|
+
let start = window.start.getTime();
|
|
121
|
+
let end = window.end.getTime();
|
|
122
|
+
if (rules?.minNoticeMinutes !== undefined) {
|
|
123
|
+
start = Math.max(start, now.getTime() + rules.minNoticeMinutes * MINUTE_MS);
|
|
124
|
+
}
|
|
125
|
+
const bufferAfterMs = bookingBufferAfterMs(rules, params.bufferAfterMs ?? 0);
|
|
126
|
+
const step = rules?.slotStepMinutes;
|
|
127
|
+
const searched = { start: new Date(start), end: new Date(end) };
|
|
128
|
+
if (rules?.maxAdvanceDays !== undefined) {
|
|
129
|
+
// The latest allowed start bounds where a slot may start; it may still end after it.
|
|
130
|
+
const latestStart = now.getTime() + rules.maxAdvanceDays * DAY_MINUTES * MINUTE_MS;
|
|
131
|
+
if (latestStart < start || end <= start)
|
|
132
|
+
return null;
|
|
133
|
+
const align = step === undefined ? undefined : (from) => nextStepStart(from, step, params.timezone);
|
|
134
|
+
return {
|
|
135
|
+
window: searched,
|
|
136
|
+
bufferAfterMs,
|
|
137
|
+
align: (from) => {
|
|
138
|
+
const aligned = align ? align(from) : from;
|
|
139
|
+
// Past the latest start, no slot: move it beyond the window so it cannot fit.
|
|
140
|
+
return aligned.getTime() > latestStart ? new Date(end + 1) : aligned;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (end <= start)
|
|
145
|
+
return null;
|
|
146
|
+
return step === undefined
|
|
147
|
+
? { window: searched, bufferAfterMs }
|
|
148
|
+
: { window: searched, bufferAfterMs, align: (from) => nextStepStart(from, step, params.timezone) };
|
|
149
|
+
}
|
package/config.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export declare function readServerUrl(env: SlotlockEnv, name: string, fallback?:
|
|
|
20
20
|
* an absolute UNTIL) is a startup error instead of silently making every resource unbookable.
|
|
21
21
|
*/
|
|
22
22
|
export declare function readAvailability(env: SlotlockEnv): WeeklyAvailabilityRule[];
|
|
23
|
+
/**
|
|
24
|
+
* A dashboard name (SLOTLOCK_DASHBOARD_NAME): 1-60 characters, none of them a control, format
|
|
25
|
+
* (bidi, zero-width) or line separator character, `<` or `>`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SLOTLOCK_DASHBOARD_NAME_PATTERN: RegExp;
|
|
23
28
|
/** The GitHub sign-in dashboard (dashboard.ts), served at `<publicUrl>/dashboard` when configured. */
|
|
24
29
|
export interface SlotlockServeDashboardConfig {
|
|
25
30
|
githubClientId: string;
|
|
@@ -27,12 +32,22 @@ export interface SlotlockServeDashboardConfig {
|
|
|
27
32
|
sessionSecret: string;
|
|
28
33
|
/** GitHub user ids that may sign in, or `'*'` for every GitHub account. */
|
|
29
34
|
allowedUsers: '*' | string[];
|
|
35
|
+
/** GitHub organization ids whose members share a tenant each (SLOTLOCK_DASHBOARD_ORGS). */
|
|
36
|
+
organizations?: string[];
|
|
37
|
+
/** What the deployment is called in the dashboard (SLOTLOCK_DASHBOARD_NAME). */
|
|
38
|
+
name?: string;
|
|
30
39
|
}
|
|
31
40
|
/**
|
|
32
41
|
* The dashboard's settings, or undefined when none is set. Setting any of them means serving the
|
|
33
42
|
* dashboard, so the rest are then required: a half-configured sign-in is a startup error.
|
|
34
43
|
*/
|
|
35
44
|
export declare function readDashboardConfig(env: SlotlockEnv): SlotlockServeDashboardConfig | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The GitHub organization ids of SLOTLOCK_DASHBOARD_ORGS, or undefined when unset. Read on its own as
|
|
47
|
+
* well: a server whose dashboard runs elsewhere (on Cloudflare Workers) still takes those
|
|
48
|
+
* organizations' keys, and only theirs.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readDashboardOrganizations(env: SlotlockEnv): string[] | undefined;
|
|
36
51
|
/** A token bucket: `perSecond` tokens come back each second, and it holds at most `burst`. */
|
|
37
52
|
export interface SlotlockRate {
|
|
38
53
|
perSecond: number;
|
|
@@ -56,9 +71,11 @@ declare const CAP_RANGES: Readonly<{
|
|
|
56
71
|
readonly tenant_reservations: readonly [1, 10000000];
|
|
57
72
|
readonly tenant_events: readonly [1, 10000000];
|
|
58
73
|
readonly tenant_coverage: readonly [1, 10000000];
|
|
74
|
+
readonly holder_holds: readonly [1, 10000];
|
|
75
|
+
readonly holder_held_hours: readonly [1, 876000];
|
|
59
76
|
}>;
|
|
60
77
|
export type SlotlockCapName = keyof typeof CAP_RANGES;
|
|
61
|
-
export declare const SLOTLOCK_CAP_NAMES: readonly ("key_lookup_queue" | "key_lookups" | "key_subscriptions" | "maintenance_runs" | "series_writes" | "subscriptions" | "tenant_coverage" | "tenant_events" | "tenant_holds" | "tenant_reservations" | "tenant_series_writes" | "tenant_subscriptions" | "tenant_transactions")[];
|
|
78
|
+
export declare const SLOTLOCK_CAP_NAMES: readonly ("holder_held_hours" | "holder_holds" | "key_lookup_queue" | "key_lookups" | "key_subscriptions" | "maintenance_runs" | "series_writes" | "subscriptions" | "tenant_coverage" | "tenant_events" | "tenant_holds" | "tenant_reservations" | "tenant_series_writes" | "tenant_subscriptions" | "tenant_transactions")[];
|
|
62
79
|
export interface SlotlockLimits {
|
|
63
80
|
/** Each rate, or null where SLOTLOCK_LIMITS turns it off. */
|
|
64
81
|
rates: Readonly<Record<SlotlockRateName, SlotlockRate | null>>;
|
|
@@ -102,6 +119,8 @@ export interface SlotlockLimits {
|
|
|
102
119
|
* agent or application writes it. Every write surface of the server shares them, so no admin
|
|
103
120
|
* key can grow the shared database without end; a write past one answers 409 with its
|
|
104
121
|
* `*_limit_reached` code.
|
|
122
|
+
* - `holder_holds` and `holder_held_hours`: one holder's (an agent's) live holds, how many and how
|
|
123
|
+
* many hours they hold together (`hold_limit_reached`, `held_time_limit_reached`).
|
|
105
124
|
*/
|
|
106
125
|
export declare const SLOTLOCK_DEFAULT_LIMITS: SlotlockLimits;
|
|
107
126
|
/**
|
package/config.js
CHANGED
|
@@ -92,6 +92,11 @@ export function readAvailability(env) {
|
|
|
92
92
|
};
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* A dashboard name (SLOTLOCK_DASHBOARD_NAME): 1-60 characters, none of them a control, format
|
|
97
|
+
* (bidi, zero-width) or line separator character, `<` or `>`.
|
|
98
|
+
*/
|
|
99
|
+
export const SLOTLOCK_DASHBOARD_NAME_PATTERN = /^[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}<>]{1,60}$/u;
|
|
95
100
|
const DASHBOARD_VARIABLES = [
|
|
96
101
|
'SLOTLOCK_GITHUB_CLIENT_ID',
|
|
97
102
|
'SLOTLOCK_GITHUB_CLIENT_SECRET',
|
|
@@ -129,13 +134,34 @@ export function readDashboardConfig(env) {
|
|
|
129
134
|
if (users === undefined || (users !== '*' && !/^[1-9]\d{0,19}(,[1-9]\d{0,19})*$/.test(users))) {
|
|
130
135
|
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_USERS is required to serve the dashboard: * for every GitHub account, or a comma-separated list of GitHub user ids (gh api users/<login> --jq .id)');
|
|
131
136
|
}
|
|
137
|
+
const orgs = readDashboardOrganizations(env);
|
|
138
|
+
const name = optional(env, 'SLOTLOCK_DASHBOARD_NAME')?.trim();
|
|
139
|
+
if (name !== undefined && name !== '' && !SLOTLOCK_DASHBOARD_NAME_PATTERN.test(name)) {
|
|
140
|
+
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_NAME must be 1-60 characters, without <, > or control or invisible characters');
|
|
141
|
+
}
|
|
132
142
|
return {
|
|
133
143
|
githubClientId,
|
|
134
144
|
githubClientSecret,
|
|
135
145
|
sessionSecret,
|
|
136
146
|
allowedUsers: users === '*' ? '*' : [...new Set(users.split(','))],
|
|
147
|
+
...(orgs !== undefined ? { organizations: orgs } : {}),
|
|
148
|
+
...(name !== undefined && name !== '' ? { name } : {}),
|
|
137
149
|
};
|
|
138
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The GitHub organization ids of SLOTLOCK_DASHBOARD_ORGS, or undefined when unset. Read on its own as
|
|
153
|
+
* well: a server whose dashboard runs elsewhere (on Cloudflare Workers) still takes those
|
|
154
|
+
* organizations' keys, and only theirs.
|
|
155
|
+
*/
|
|
156
|
+
export function readDashboardOrganizations(env) {
|
|
157
|
+
const orgs = optional(env, 'SLOTLOCK_DASHBOARD_ORGS')?.replaceAll(' ', '');
|
|
158
|
+
if (orgs === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
if (!/^[1-9]\d{0,19}(,[1-9]\d{0,19})*$/.test(orgs)) {
|
|
161
|
+
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_ORGS must be a comma-separated list of GitHub organization ids (gh api orgs/<login> --jq .id)');
|
|
162
|
+
}
|
|
163
|
+
return [...new Set(orgs.split(','))];
|
|
164
|
+
}
|
|
139
165
|
/** The token buckets `slotlock serve` keeps (SLOTLOCK_LIMITS), each counted per its own key. */
|
|
140
166
|
export const SLOTLOCK_RATE_NAMES = Object.freeze([
|
|
141
167
|
'address',
|
|
@@ -165,6 +191,8 @@ const CAP_RANGES = Object.freeze({
|
|
|
165
191
|
tenant_reservations: [1, 10_000_000],
|
|
166
192
|
tenant_events: [1, 10_000_000],
|
|
167
193
|
tenant_coverage: [1, 10_000_000],
|
|
194
|
+
holder_holds: [1, 10_000],
|
|
195
|
+
holder_held_hours: [1, 876_000],
|
|
168
196
|
});
|
|
169
197
|
export const SLOTLOCK_CAP_NAMES = Object.freeze(Object.keys(CAP_RANGES));
|
|
170
198
|
/**
|
|
@@ -205,6 +233,8 @@ export const SLOTLOCK_CAP_NAMES = Object.freeze(Object.keys(CAP_RANGES));
|
|
|
205
233
|
* agent or application writes it. Every write surface of the server shares them, so no admin
|
|
206
234
|
* key can grow the shared database without end; a write past one answers 409 with its
|
|
207
235
|
* `*_limit_reached` code.
|
|
236
|
+
* - `holder_holds` and `holder_held_hours`: one holder's (an agent's) live holds, how many and how
|
|
237
|
+
* many hours they hold together (`hold_limit_reached`, `held_time_limit_reached`).
|
|
208
238
|
*/
|
|
209
239
|
export const SLOTLOCK_DEFAULT_LIMITS = Object.freeze({
|
|
210
240
|
rates: Object.freeze({
|
|
@@ -235,6 +265,9 @@ export const SLOTLOCK_DEFAULT_LIMITS = Object.freeze({
|
|
|
235
265
|
tenant_reservations: 100_000,
|
|
236
266
|
tenant_events: 100_000,
|
|
237
267
|
tenant_coverage: 100_000,
|
|
268
|
+
// One agent's live holds: 20 at once (candidates for a few bookings), holding 90 days in all.
|
|
269
|
+
holder_holds: 20,
|
|
270
|
+
holder_held_hours: 2_160,
|
|
238
271
|
}),
|
|
239
272
|
});
|
|
240
273
|
/**
|
package/dashboard.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { SlotlockApiKeyStore } from './api-keys.js';
|
|
2
|
-
import
|
|
3
|
-
import type
|
|
2
|
+
import { type SlotlockBookingRules } from './booking-rules.js';
|
|
3
|
+
import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
|
|
4
|
+
import type { SlotlockSql, SlotlockStore, SlotlockWaitlistEntry } from './store.js';
|
|
5
|
+
import type { Interval, SlotlockAgendaEntry, SlotlockResource, WeeklyAvailabilityRule } from './types.js';
|
|
4
6
|
/** Resources one dashboard tenant may hold. */
|
|
5
7
|
export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 100;
|
|
6
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* A tenant's calendar resources, as the dashboard lists and adds them. Each write takes, last, the
|
|
10
|
+
* person who made it (`github:<user id>`) as `actor`, to record on the event feed.
|
|
11
|
+
*/
|
|
7
12
|
export interface SlotlockDashboardResources {
|
|
8
13
|
list(tenantRef: string): Promise<SlotlockResource[]>;
|
|
9
14
|
/**
|
|
@@ -11,7 +16,7 @@ export interface SlotlockDashboardResources {
|
|
|
11
16
|
* (an error with `code: 'resource_limit_reached'`) once the tenant has
|
|
12
17
|
* SLOTLOCK_DASHBOARD_MAX_RESOURCES, however many adds race, on however many servers.
|
|
13
18
|
*/
|
|
14
|
-
add(tenantRef: string, externalRef: string, timezone: string): Promise<SlotlockResource>;
|
|
19
|
+
add(tenantRef: string, externalRef: string, timezone: string, actor?: string): Promise<SlotlockResource>;
|
|
15
20
|
/**
|
|
16
21
|
* Set the bookable hours of one of the tenant's resources (`{ id }`) or of all of them; `null`
|
|
17
22
|
* hands them back to the server's and `[]` closes. Returns how many resources changed: 0 for an
|
|
@@ -19,7 +24,32 @@ export interface SlotlockDashboardResources {
|
|
|
19
24
|
*/
|
|
20
25
|
setHours(tenantRef: string, target: {
|
|
21
26
|
id: string;
|
|
22
|
-
} | 'all', rules: WeeklyAvailabilityRule[] | null): Promise<number>;
|
|
27
|
+
} | 'all', rules: WeeklyAvailabilityRule[] | null, actor?: string): Promise<number>;
|
|
28
|
+
/** What occupies the tenant's resources in `window`, by start: the overview's agenda. */
|
|
29
|
+
agenda?(tenantRef: string, window: Interval, limit: number): Promise<SlotlockAgendaEntry[]>;
|
|
30
|
+
/** The tenant's latest changes, newest first: the overview's activity. */
|
|
31
|
+
recentChanges?(tenantRef: string, limit: number): Promise<SlotlockChange[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Set the booking rules of one of the tenant's resources (null clears them): the resource, or
|
|
34
|
+
* null when it is not the tenant's. Invalid rules throw with `code: 'invalid_booking_rules'`.
|
|
35
|
+
*/
|
|
36
|
+
setRules?(tenantRef: string, id: string, rules: SlotlockBookingRules | null, actor?: string): Promise<SlotlockResource | null>;
|
|
37
|
+
/** Put one of the tenant's resources in a pool, or none: as setRules; `code: 'invalid_pool'`. */
|
|
38
|
+
setPool?(tenantRef: string, id: string, pool: string | null, actor?: string): Promise<SlotlockResource | null>;
|
|
39
|
+
/** The tenant's waitlist, oldest first, and leaving it (false when the entry is not the tenant's). */
|
|
40
|
+
waitlist?: {
|
|
41
|
+
list(tenantRef: string, limit: number): Promise<SlotlockWaitlistEntry[]>;
|
|
42
|
+
leave(tenantRef: string, id: string, actor?: string): Promise<boolean>;
|
|
43
|
+
};
|
|
44
|
+
/** The tenant's webhook endpoints, as the event feed keeps them. */
|
|
45
|
+
webhooks?: {
|
|
46
|
+
list(tenantRef: string): Promise<SlotlockWebhookEndpoint[]>;
|
|
47
|
+
create(tenantRef: string, url: string, eventTypes: readonly string[] | undefined, actor?: string): Promise<CreateWebhookEndpointResult>;
|
|
48
|
+
remove(tenantRef: string, id: string, actor?: string): Promise<boolean>;
|
|
49
|
+
enable(tenantRef: string, id: string, actor?: string): Promise<SlotlockWebhookEndpoint | null | {
|
|
50
|
+
code: 'changes_pruned';
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
23
53
|
}
|
|
24
54
|
/**
|
|
25
55
|
* What the dashboard records so a sign-in callback works once, a sent form runs once and a
|
|
@@ -47,6 +77,8 @@ export interface SlotlockDashboardState {
|
|
|
47
77
|
export interface SlotlockDashboardOptions {
|
|
48
78
|
/** Where people reach the server; the dashboard lives at `<publicUrl>/dashboard`. */
|
|
49
79
|
publicUrl: string;
|
|
80
|
+
/** What the deployment is called in the dashboard: 1-60 characters. Defaults to `Slotlock`. */
|
|
81
|
+
name?: string;
|
|
50
82
|
/**
|
|
51
83
|
* Where agents reach the server's MCP and A2A endpoints, which the dashboard shows, when it is
|
|
52
84
|
* served from another origin (on Cloudflare Workers, with the server elsewhere). Defaults to
|
|
@@ -63,11 +95,23 @@ export interface SlotlockDashboardOptions {
|
|
|
63
95
|
authorizeUrl?: string;
|
|
64
96
|
tokenUrl?: string;
|
|
65
97
|
userUrl?: string;
|
|
98
|
+
membershipsUrl?: string;
|
|
66
99
|
};
|
|
67
100
|
/** Seals sessions and sign-in state and derives CSRF tokens: 32+ random characters. */
|
|
68
101
|
sessionSecret: string;
|
|
69
102
|
/** GitHub user ids (numbers, as strings) that may sign in, or `'*'` for every GitHub account. */
|
|
70
103
|
allowedUsers: '*' | readonly string[];
|
|
104
|
+
/**
|
|
105
|
+
* GitHub organization ids (numbers, as strings) this dashboard serves. With them, a tenant is an
|
|
106
|
+
* organization (`github-org:<id>`), not a person: sign-in asks for `read:org`, and a person signs
|
|
107
|
+
* in to the served organizations they are an active member of, sharing each one's keys and
|
|
108
|
+
* resources with its other members: owners and members, not billing managers, from the first
|
|
109
|
+
* 100 memberships GitHub lists, up to 20 served organizations. Membership is read at each sign-in; leaving an organization
|
|
110
|
+
* ends access at the next sign-in, at most a session (12 hours) later, and removing an
|
|
111
|
+
* organization from this list signs its sessions out at once. Without it, each person is their
|
|
112
|
+
* own tenant (`github:<id>`).
|
|
113
|
+
*/
|
|
114
|
+
organizations?: readonly string[];
|
|
71
115
|
keys: SlotlockApiKeyStore;
|
|
72
116
|
resources: SlotlockDashboardResources;
|
|
73
117
|
/** Shared by every server behind this origin: see SlotlockDashboardState. */
|
|
@@ -98,6 +142,18 @@ export interface SlotlockDashboard {
|
|
|
98
142
|
* in the database (a READ COMMITTED transaction, under a per-tenant lock).
|
|
99
143
|
*/
|
|
100
144
|
export declare function createSlotlockDashboardResources(store: SlotlockStore): SlotlockDashboardResources;
|
|
145
|
+
/**
|
|
146
|
+
* Records accepted write confirmations (`SlotlockAgentConfirmationOptions.consumeAcceptance`) in
|
|
147
|
+
* Postgres, shared by every server on the database: true the first time a confirmation's nonce is
|
|
148
|
+
* recorded, false while it stays recorded, so an accepted confirmation runs its write once on any
|
|
149
|
+
* server. Kept until the confirmation's own expiry, through the same function and table as the
|
|
150
|
+
* dashboard's records (only a SHA-256 digest of the nonce is stored); pass a client, not a
|
|
151
|
+
* transaction.
|
|
152
|
+
*/
|
|
153
|
+
export declare function createSlotlockAcceptanceRecords(sql: SlotlockSql): (acceptance: {
|
|
154
|
+
nonce: string;
|
|
155
|
+
expiresAt: Date;
|
|
156
|
+
}) => Promise<boolean>;
|
|
101
157
|
/**
|
|
102
158
|
* The dashboard's records in Postgres, shared by every server on the database. Run `applySchema()`
|
|
103
159
|
* and `grantApplicationRole()` first: the serving role reaches the records only through
|