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/config.js
CHANGED
|
@@ -97,15 +97,33 @@ export function readAvailability(env) {
|
|
|
97
97
|
* (bidi, zero-width) or line separator character, `<` or `>`.
|
|
98
98
|
*/
|
|
99
99
|
export const SLOTLOCK_DASHBOARD_NAME_PATTERN = /^[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}<>]{1,60}$/u;
|
|
100
|
+
/**
|
|
101
|
+
* Setting any of these means serving the dashboard. SLOTLOCK_DASHBOARD_USERS is not one: alone, it
|
|
102
|
+
* names whose keys a server takes when the dashboard runs elsewhere (readDashboardUsers).
|
|
103
|
+
*/
|
|
100
104
|
const DASHBOARD_VARIABLES = [
|
|
101
105
|
'SLOTLOCK_GITHUB_CLIENT_ID',
|
|
102
106
|
'SLOTLOCK_GITHUB_CLIENT_SECRET',
|
|
103
107
|
'SLOTLOCK_SESSION_SECRET',
|
|
104
|
-
'SLOTLOCK_DASHBOARD_USERS',
|
|
105
108
|
];
|
|
106
109
|
/**
|
|
107
|
-
*
|
|
108
|
-
* dashboard
|
|
110
|
+
* SLOTLOCK_DASHBOARD_USERS: `'*'`, GitHub user ids, or undefined when it is not set. A server whose
|
|
111
|
+
* dashboard runs elsewhere (a Worker) reads it too, so that removing a person there stops their keys.
|
|
112
|
+
*/
|
|
113
|
+
export function readDashboardUsers(env) {
|
|
114
|
+
const users = optional(env, 'SLOTLOCK_DASHBOARD_USERS')?.replaceAll(' ', '');
|
|
115
|
+
if (users === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
const person = '(?:[1-9]\\d{0,19}|google:[A-Za-z0-9._-]{1,255})';
|
|
118
|
+
if (users !== '*' && !new RegExp(`^${person}(,${person})*$`).test(users)) {
|
|
119
|
+
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_USERS must be * for every account, or a comma-separated list of GitHub user ids (gh api users/<login> --jq .id) and Google accounts as google:<subject>');
|
|
120
|
+
}
|
|
121
|
+
return users === '*' ? '*' : [...new Set(users.split(','))];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The dashboard's settings, or undefined when none of DASHBOARD_VARIABLES is set. Setting any of
|
|
125
|
+
* them means serving the dashboard, so the rest, and SLOTLOCK_DASHBOARD_USERS, are then required: a
|
|
126
|
+
* half-configured sign-in is a startup error.
|
|
109
127
|
*/
|
|
110
128
|
export function readDashboardConfig(env) {
|
|
111
129
|
if (DASHBOARD_VARIABLES.every((name) => optional(env, name) === undefined))
|
|
@@ -118,6 +136,14 @@ export function readDashboardConfig(env) {
|
|
|
118
136
|
if (githubClientSecret === undefined) {
|
|
119
137
|
throw new SlotlockConfigError('SLOTLOCK_GITHUB_CLIENT_SECRET is required to serve the dashboard: a client secret of that GitHub OAuth app');
|
|
120
138
|
}
|
|
139
|
+
const googleClientId = optional(env, 'SLOTLOCK_GOOGLE_CLIENT_ID');
|
|
140
|
+
const googleClientSecret = readSecret(env, 'SLOTLOCK_GOOGLE_CLIENT_SECRET');
|
|
141
|
+
if ((googleClientId === undefined) !== (googleClientSecret === undefined)) {
|
|
142
|
+
throw new SlotlockConfigError('SLOTLOCK_GOOGLE_CLIENT_ID and SLOTLOCK_GOOGLE_CLIENT_SECRET go together: an OAuth client of a Google Cloud project whose redirect URI is <SLOTLOCK_PUBLIC_URL>/dashboard/callback');
|
|
143
|
+
}
|
|
144
|
+
if (googleClientId !== undefined && !/^[A-Za-z0-9._-]{1,200}$/.test(googleClientId)) {
|
|
145
|
+
throw new SlotlockConfigError('SLOTLOCK_GOOGLE_CLIENT_ID must be a Google OAuth client id');
|
|
146
|
+
}
|
|
121
147
|
const sessionSecret = readSecret(env, 'SLOTLOCK_SESSION_SECRET');
|
|
122
148
|
if (sessionSecret === undefined) {
|
|
123
149
|
throw new SlotlockConfigError(`SLOTLOCK_SESSION_SECRET is required to serve the dashboard: it signs sign-in sessions; ${SECRET_HINT}`);
|
|
@@ -126,12 +152,19 @@ export function readDashboardConfig(env) {
|
|
|
126
152
|
env.SLOTLOCK_GITHUB_CLIENT_SECRET,
|
|
127
153
|
env.SLOTLOCK_AUTH_TOKEN,
|
|
128
154
|
env.SLOTLOCK_CONFIRMATION_SECRET,
|
|
155
|
+
...(googleClientSecret !== undefined ? [googleClientSecret] : []),
|
|
129
156
|
];
|
|
130
157
|
if (others.includes(sessionSecret)) {
|
|
131
158
|
throw new SlotlockConfigError('SLOTLOCK_SESSION_SECRET must differ from every other secret');
|
|
132
159
|
}
|
|
133
|
-
|
|
134
|
-
|
|
160
|
+
let users;
|
|
161
|
+
try {
|
|
162
|
+
users = readDashboardUsers(env);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
users = undefined;
|
|
166
|
+
}
|
|
167
|
+
if (users === undefined) {
|
|
135
168
|
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)');
|
|
136
169
|
}
|
|
137
170
|
const assistantSecret = readSecret(env, 'SLOTLOCK_ASSISTANT_SECRET');
|
|
@@ -139,26 +172,83 @@ export function readDashboardConfig(env) {
|
|
|
139
172
|
[sessionSecret, ...others].includes(assistantSecret)) {
|
|
140
173
|
throw new SlotlockConfigError('SLOTLOCK_ASSISTANT_SECRET must differ from every other secret');
|
|
141
174
|
}
|
|
175
|
+
const previousSecrets = readAssistantPreviousSecrets(env, assistantSecret, [sessionSecret, ...others]);
|
|
142
176
|
const privateUrls = optional(env, 'SLOTLOCK_ASSISTANT_PRIVATE_URLS') ?? 'off';
|
|
143
177
|
if (privateUrls !== 'on' && privateUrls !== 'off') {
|
|
144
178
|
throw new SlotlockConfigError('SLOTLOCK_ASSISTANT_PRIVATE_URLS must be on or off');
|
|
145
179
|
}
|
|
146
180
|
const orgs = readDashboardOrganizations(env);
|
|
181
|
+
if (Array.isArray(orgs) && googleClientId !== undefined) {
|
|
182
|
+
throw new SlotlockConfigError('SLOTLOCK_GOOGLE_CLIENT_ID does not go with a list of SLOTLOCK_DASHBOARD_ORGS: those organizations are GitHub\'s; use SLOTLOCK_DASHBOARD_ORGS=enabled to have both');
|
|
183
|
+
}
|
|
147
184
|
const name = optional(env, 'SLOTLOCK_DASHBOARD_NAME')?.trim();
|
|
148
185
|
if (name !== undefined && name !== '' && !SLOTLOCK_DASHBOARD_NAME_PATTERN.test(name)) {
|
|
149
186
|
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_NAME must be 1-60 characters, without <, > or control or invisible characters');
|
|
150
187
|
}
|
|
188
|
+
const termsUrl = readPolicyUrl(env, 'SLOTLOCK_DASHBOARD_TERMS_URL');
|
|
189
|
+
const privacyUrl = readPolicyUrl(env, 'SLOTLOCK_DASHBOARD_PRIVACY_URL');
|
|
151
190
|
return {
|
|
152
191
|
githubClientId,
|
|
153
192
|
githubClientSecret,
|
|
154
193
|
sessionSecret,
|
|
155
|
-
allowedUsers: users
|
|
194
|
+
allowedUsers: users,
|
|
195
|
+
...(googleClientId !== undefined && googleClientSecret !== undefined
|
|
196
|
+
? { google: { clientId: googleClientId, clientSecret: googleClientSecret } }
|
|
197
|
+
: {}),
|
|
156
198
|
...(orgs !== undefined ? { organizations: orgs } : {}),
|
|
157
199
|
...(name !== undefined && name !== '' ? { name } : {}),
|
|
200
|
+
...(termsUrl !== undefined ? { termsUrl } : {}),
|
|
201
|
+
...(privacyUrl !== undefined ? { privacyUrl } : {}),
|
|
158
202
|
...(assistantSecret !== undefined ? { assistantSecret } : {}),
|
|
203
|
+
...(previousSecrets.length > 0 ? { assistantPreviousSecrets: previousSecrets } : {}),
|
|
159
204
|
...(assistantSecret !== undefined && privateUrls === 'on' ? { assistantPrivateUrls: true } : {}),
|
|
160
205
|
};
|
|
161
206
|
}
|
|
207
|
+
/** Previous assistant secrets a deployment may keep (assistant.ts SLOTLOCK_ASSISTANT_MAX_PREVIOUS_SECRETS). */
|
|
208
|
+
const MAX_ASSISTANT_PREVIOUS_SECRETS = 3;
|
|
209
|
+
/**
|
|
210
|
+
* SLOTLOCK_ASSISTANT_PREVIOUS_SECRETS: up to three earlier values of SLOTLOCK_ASSISTANT_SECRET,
|
|
211
|
+
* comma-separated, newest first, each as strong as a secret must be and unlike every other secret.
|
|
212
|
+
* Only with SLOTLOCK_ASSISTANT_SECRET: without a current secret nothing would seal the keys again.
|
|
213
|
+
*/
|
|
214
|
+
function readAssistantPreviousSecrets(env, current, others) {
|
|
215
|
+
const name = 'SLOTLOCK_ASSISTANT_PREVIOUS_SECRETS';
|
|
216
|
+
const value = env[name];
|
|
217
|
+
if (value === undefined || value.trim() === '')
|
|
218
|
+
return [];
|
|
219
|
+
if (current === undefined) {
|
|
220
|
+
throw new SlotlockConfigError(`${name} needs SLOTLOCK_ASSISTANT_SECRET: the secret keys are sealed again under`);
|
|
221
|
+
}
|
|
222
|
+
const secrets = value.split(',').map((part) => readSecret({ [name]: part }, name));
|
|
223
|
+
if (secrets.some((secret) => secret === undefined)) {
|
|
224
|
+
throw new SlotlockConfigError(`${name} must be comma-separated secrets, with no empty one`);
|
|
225
|
+
}
|
|
226
|
+
const listed = secrets;
|
|
227
|
+
if (listed.length > MAX_ASSISTANT_PREVIOUS_SECRETS) {
|
|
228
|
+
throw new SlotlockConfigError(`${name} takes at most ${String(MAX_ASSISTANT_PREVIOUS_SECRETS)} secrets`);
|
|
229
|
+
}
|
|
230
|
+
if (new Set(listed).size !== listed.length || listed.some((secret) => secret === current || others.includes(secret))) {
|
|
231
|
+
throw new SlotlockConfigError(`${name} must differ from each other, from SLOTLOCK_ASSISTANT_SECRET and from every other secret`);
|
|
232
|
+
}
|
|
233
|
+
return listed;
|
|
234
|
+
}
|
|
235
|
+
/** A policy page the sign-in page links: an https URL, or undefined when the setting is empty. */
|
|
236
|
+
function readPolicyUrl(env, name) {
|
|
237
|
+
const value = optional(env, name)?.trim();
|
|
238
|
+
if (value === undefined || value === '')
|
|
239
|
+
return undefined;
|
|
240
|
+
let url;
|
|
241
|
+
try {
|
|
242
|
+
url = new URL(value);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
throw new SlotlockConfigError(`${name} must be an https URL`);
|
|
246
|
+
}
|
|
247
|
+
if (url.protocol !== 'https:' || url.username !== '' || url.password !== '') {
|
|
248
|
+
throw new SlotlockConfigError(`${name} must be an https URL`);
|
|
249
|
+
}
|
|
250
|
+
return url.href;
|
|
251
|
+
}
|
|
162
252
|
/**
|
|
163
253
|
* The GitHub organization ids of SLOTLOCK_DASHBOARD_ORGS, or undefined when unset. Read on its own as
|
|
164
254
|
* well: a server whose dashboard runs elsewhere (on Cloudflare Workers) still takes those
|
|
@@ -168,8 +258,10 @@ export function readDashboardOrganizations(env) {
|
|
|
168
258
|
const orgs = optional(env, 'SLOTLOCK_DASHBOARD_ORGS')?.replaceAll(' ', '');
|
|
169
259
|
if (orgs === undefined)
|
|
170
260
|
return undefined;
|
|
261
|
+
if (orgs === 'enabled')
|
|
262
|
+
return 'enabled';
|
|
171
263
|
if (!/^[1-9]\d{0,19}(,[1-9]\d{0,19})*$/.test(orgs)) {
|
|
172
|
-
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_ORGS must be a comma-separated list of GitHub organization ids (gh api orgs/<login> --jq .id)');
|
|
264
|
+
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_ORGS must be a comma-separated list of GitHub organization ids (gh api orgs/<login> --jq .id), or enabled');
|
|
173
265
|
}
|
|
174
266
|
return [...new Set(orgs.split(','))];
|
|
175
267
|
}
|
|
@@ -372,3 +464,18 @@ export function readTrustedProxyHeader(env) {
|
|
|
372
464
|
}
|
|
373
465
|
return value.toLowerCase();
|
|
374
466
|
}
|
|
467
|
+
/** The request field that carries SLOTLOCK_TRUSTED_PROXY_SECRET; not cf-…, which Cloudflare's rules may not set. */
|
|
468
|
+
export const SLOTLOCK_TRUSTED_PROXY_PROOF_FIELD = 'x-slotlock-proxy-proof';
|
|
469
|
+
/**
|
|
470
|
+
* SLOTLOCK_TRUSTED_PROXY_SECRET: a value the proxy in front adds to every request in
|
|
471
|
+
* SLOTLOCK_TRUSTED_PROXY_PROOF_FIELD, such as a Cloudflare transform rule. With it, the trusted
|
|
472
|
+
* proxy field is believed only beside that value, so a process that reaches the server without
|
|
473
|
+
* passing the proxy (another container on its network) cannot choose its own client address.
|
|
474
|
+
*/
|
|
475
|
+
export function readTrustedProxySecret(env, trustedProxyHeader) {
|
|
476
|
+
const value = readSecret(env, 'SLOTLOCK_TRUSTED_PROXY_SECRET');
|
|
477
|
+
if (value !== undefined && trustedProxyHeader === undefined) {
|
|
478
|
+
throw new SlotlockConfigError('SLOTLOCK_TRUSTED_PROXY_SECRET proves SLOTLOCK_TRUSTED_PROXY_HEADER: set both, or neither');
|
|
479
|
+
}
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SLOTLOCK_DASHBOARD_ICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><rect width=\"64\" height=\"64\" rx=\"14\" fill=\"#111110\"/><g transform=\"translate(12.2 14.1) scale(0.465)\"><path fill=\"#F6F4EE\" d=\"M13.13 34.46L72.2 34.46C79.45 34.46 85.32 40.33 85.32 47.58L85.32 63.99C85.32 71.24 79.45 77.12 72.2 77.12L13.13 77.12C5.88 77.12 0 71.24 0 63.99L0 47.58C0 40.33 5.88 34.46 13.13 34.46ZM22.97 39.38L22.97 19.69C22.97 8.82 31.79 0 42.66 0C53.54 0 62.35 8.82 62.35 19.69L62.35 39.38L56.61 39.38L56.61 19.69C56.61 11.99 50.36 5.74 42.66 5.74C34.96 5.74 28.71 11.99 28.71 19.69L28.71 39.38Z\"/><path fill=\"#FF5B2E\" d=\"M19.69 52.51L65.63 52.51C67.45 52.51 68.91 53.98 68.91 55.79C68.91 57.6 67.45 59.07 65.63 59.07L19.69 59.07C17.88 59.07 16.41 57.6 16.41 55.79C16.41 53.98 17.88 52.51 19.69 52.51Z\"/></g></svg>\n";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The Slotlock mark the dashboard serves as its favicon. A copy of site/public/favicon.svg, which a
|
|
2
|
+
// test keeps identical; the dashboard's Content-Security-Policy allows images from its own origin
|
|
3
|
+
// only, so the mark is served, not inlined.
|
|
4
|
+
export const SLOTLOCK_DASHBOARD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#111110"/><g transform="translate(12.2 14.1) scale(0.465)"><path fill="#F6F4EE" d="M13.13 34.46L72.2 34.46C79.45 34.46 85.32 40.33 85.32 47.58L85.32 63.99C85.32 71.24 79.45 77.12 72.2 77.12L13.13 77.12C5.88 77.12 0 71.24 0 63.99L0 47.58C0 40.33 5.88 34.46 13.13 34.46ZM22.97 39.38L22.97 19.69C22.97 8.82 31.79 0 42.66 0C53.54 0 62.35 8.82 62.35 19.69L62.35 39.38L56.61 39.38L56.61 19.69C56.61 11.99 50.36 5.74 42.66 5.74C34.96 5.74 28.71 11.99 28.71 19.69L28.71 39.38Z"/><path fill="#FF5B2E" d="M19.69 52.51L65.63 52.51C67.45 52.51 68.91 53.98 68.91 55.79C68.91 57.6 67.45 59.07 65.63 59.07L19.69 59.07C17.88 59.07 16.41 57.6 16.41 55.79C16.41 53.98 17.88 52.51 19.69 52.51Z"/></g></svg>
|
|
5
|
+
`;
|
package/dashboard.d.ts
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type
|
|
1
|
+
import type { SlotlockDashboardTeams } from './teams.js';
|
|
2
|
+
import { type SlotlockApiKeyStore } from './api-keys.js';
|
|
3
|
+
import { type SlotlockAgentCalendarBackend, type SlotlockAgentOperation, type SlotlockAgentOperationDispatchOptions, type SlotlockAgentPrincipal, type SlotlockAgentUsage } from './agent-server.js';
|
|
3
4
|
import { type SlotlockAssistantStore } from './assistant.js';
|
|
4
5
|
import { type SlotlockRate } from './config.js';
|
|
5
6
|
import { type SlotlockBookingRules } from './booking-rules.js';
|
|
7
|
+
import { type SlotlockTenantPlan } from './plans.js';
|
|
8
|
+
import { type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceProfileInput } from './resource-profile.js';
|
|
9
|
+
import { type SlotlockUsageRow } from './usage.js';
|
|
6
10
|
import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
|
|
7
11
|
import type { SlotlockSql, SlotlockStore, SlotlockWaitlistEntry } from './store.js';
|
|
8
|
-
import
|
|
12
|
+
import { type SlotlockSandboxScenario } from './sandbox.js';
|
|
13
|
+
import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
|
|
9
14
|
/** Resources one dashboard tenant may hold. */
|
|
10
|
-
export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES =
|
|
15
|
+
export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 500;
|
|
11
16
|
/**
|
|
12
17
|
* A tenant's calendar resources, as the dashboard lists and adds them. Each write takes, last, the
|
|
13
18
|
* person who made it (`github:<user id>`) as `actor`, to record on the event feed.
|
|
14
19
|
*/
|
|
15
20
|
export interface SlotlockDashboardResources {
|
|
16
21
|
list(tenantRef: string): Promise<SlotlockResource[]>;
|
|
22
|
+
/** How many resources the tenant has, archived ones included, as a resource limit counts them. */
|
|
23
|
+
count?(tenantRef: string): Promise<number>;
|
|
24
|
+
/** The tenant's metered usage from `from` to `to`, UTC days inclusive (usage.ts). */
|
|
25
|
+
usage?(tenantRef: string, from: string, to: string): Promise<SlotlockUsageRow[]>;
|
|
17
26
|
/**
|
|
18
27
|
* Create the resource, or re-zone the tenant's resource with that reference. Refuse a new one
|
|
19
28
|
* (an error with `code: 'resource_limit_reached'`) once the tenant has
|
|
@@ -39,8 +48,84 @@ export interface SlotlockDashboardResources {
|
|
|
39
48
|
setRules?(tenantRef: string, id: string, rules: SlotlockBookingRules | null, actor?: string): Promise<SlotlockResource | null>;
|
|
40
49
|
/** The tenant's default booking rules and its pools', which a resource's own rules overlay. */
|
|
41
50
|
ruleDefaults?(tenantRef: string): Promise<SlotlockBookingRuleDefaults>;
|
|
51
|
+
/**
|
|
52
|
+
* Change one of the tenant's resources' profile (resource-profile.ts): the fields given, keeping
|
|
53
|
+
* the rest. As setRules; `code: 'invalid_resource_profile'`, with the `field`.
|
|
54
|
+
*/
|
|
55
|
+
setProfile?(tenantRef: string, id: string, profile: SlotlockResourceProfileInput, actor?: string): Promise<SlotlockResource | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Make one of the tenant's resources a part of a composite (RFC 0003 G8), or no part. A refusal
|
|
58
|
+
* carries the store's `code`: `parent_not_found`, `composite_too_deep`, `composite_parts_limit`
|
|
59
|
+
* or `composite_busy`.
|
|
60
|
+
*/
|
|
61
|
+
setParent?(tenantRef: string, id: string, parentId: string | null, actor?: string): Promise<SlotlockResource | null>;
|
|
42
62
|
/** Put one of the tenant's resources in a pool, or none: as setRules; `code: 'invalid_pool'`. */
|
|
43
63
|
setPool?(tenantRef: string, id: string, pool: string | null, actor?: string): Promise<SlotlockResource | null>;
|
|
64
|
+
/**
|
|
65
|
+
* The tenant's locations and groups (RFC 0003 G2, G3), and where each resource is in them. A
|
|
66
|
+
* refusal carries the store's `code`: `invalid_location` or `invalid_group` (with the `field`),
|
|
67
|
+
* `location_not_found`, `location_too_deep`, `location_cycle`, `location_limit_reached`,
|
|
68
|
+
* `location_in_use`, `group_not_found`, `group_limit_reached` or `resource_group_limit_reached`.
|
|
69
|
+
*/
|
|
70
|
+
grouping?: {
|
|
71
|
+
/** Each under its parent, depth first, with how many resources are at each. */
|
|
72
|
+
locations(tenantRef: string): Promise<SlotlockLocation[]>;
|
|
73
|
+
/** Create the location with this reference, or change its name, parent and time zone. */
|
|
74
|
+
putLocation(tenantRef: string, location: {
|
|
75
|
+
ref: string;
|
|
76
|
+
name: string;
|
|
77
|
+
parentRef: string | null;
|
|
78
|
+
timezone: string | null;
|
|
79
|
+
}, actor?: string): Promise<SlotlockLocation>;
|
|
80
|
+
/** False when the tenant has no such location. */
|
|
81
|
+
deleteLocation(tenantRef: string, ref: string, actor?: string): Promise<boolean>;
|
|
82
|
+
/** By reference, with how many resources are in each. */
|
|
83
|
+
groups(tenantRef: string): Promise<SlotlockResourceGroup[]>;
|
|
84
|
+
/** Create the group with this reference, or change its name and description. */
|
|
85
|
+
putGroup(tenantRef: string, group: {
|
|
86
|
+
ref: string;
|
|
87
|
+
name: string;
|
|
88
|
+
description: string | null;
|
|
89
|
+
}, actor?: string): Promise<SlotlockResourceGroup>;
|
|
90
|
+
/** Delete a group, its resources left as they are: false when the tenant has no such group. */
|
|
91
|
+
deleteGroup(tenantRef: string, ref: string, actor?: string): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Put one of the tenant's resources at a location (or none) and in exactly these groups, in one
|
|
94
|
+
* transaction: the resource as it then is, or null when it is not the tenant's.
|
|
95
|
+
*/
|
|
96
|
+
place(tenantRef: string, id: string, place: {
|
|
97
|
+
location: string | null;
|
|
98
|
+
groups: readonly string[];
|
|
99
|
+
}, actor?: string): Promise<SlotlockResource | null>;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The tenant's approvals (RFC 0004 A4), newest first, and deciding one: a grant makes the booking
|
|
103
|
+
* as the agent asked for it; a refusal says why (approval_not_pending, overlap, a booking rule).
|
|
104
|
+
*/
|
|
105
|
+
approvals?: {
|
|
106
|
+
list(tenantRef: string, limit: number): Promise<SlotlockApproval[]>;
|
|
107
|
+
decide(tenantRef: string, id: string, decision: 'grant' | 'decline', actor?: string, reason?: string): Promise<{
|
|
108
|
+
ok: true;
|
|
109
|
+
} | {
|
|
110
|
+
ok: false;
|
|
111
|
+
code: string;
|
|
112
|
+
}>;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The sandbox (RFC 0004 A6), for a test environment only: fill it with a scenario, empty it, and
|
|
116
|
+
* make what would lapse within some minutes lapse now. Each refuses a live tenant.
|
|
117
|
+
*/
|
|
118
|
+
sandbox?: {
|
|
119
|
+
seed(tenantRef: string, scenario: SlotlockSandboxScenario): Promise<{
|
|
120
|
+
resources: number;
|
|
121
|
+
bookings: number;
|
|
122
|
+
}>;
|
|
123
|
+
reset(tenantRef: string): Promise<void>;
|
|
124
|
+
fastForward(tenantRef: string, minutes: number): Promise<{
|
|
125
|
+
holds: number;
|
|
126
|
+
approvals: number;
|
|
127
|
+
}>;
|
|
128
|
+
};
|
|
44
129
|
/** The tenant's waitlist, oldest first, and leaving it (false when the entry is not the tenant's). */
|
|
45
130
|
waitlist?: {
|
|
46
131
|
list(tenantRef: string, limit: number): Promise<SlotlockWaitlistEntry[]>;
|
|
@@ -91,6 +176,18 @@ export interface SlotlockDashboardState {
|
|
|
91
176
|
export interface SlotlockDashboardAssistantOptions {
|
|
92
177
|
/** Seals each tenant's API key (SLOTLOCK_ASSISTANT_SECRET): 32+ random characters. */
|
|
93
178
|
secret: string;
|
|
179
|
+
/**
|
|
180
|
+
* Secrets keys may still be sealed under (SLOTLOCK_ASSISTANT_PREVIOUS_SECRETS), newest first, at
|
|
181
|
+
* most SLOTLOCK_ASSISTANT_MAX_PREVIOUS_SECRETS: a key one of them opens is used, and sealed again
|
|
182
|
+
* under `secret` in the same request, so changing the secret makes no owner enter a key again.
|
|
183
|
+
* Drop an old secret once every tenant has used the assistant since the change, or keys still
|
|
184
|
+
* sealed under it stop opening.
|
|
185
|
+
*/
|
|
186
|
+
previousSecrets?: readonly string[];
|
|
187
|
+
/** Each operation the assistant runs, for usage metering (the agent server's `onUsage`). */
|
|
188
|
+
onUsage?: (usage: SlotlockAgentUsage) => void;
|
|
189
|
+
/** Each message a member sends that the assistant answers, for usage metering. Never awaited. */
|
|
190
|
+
onMessage?: (tenantRef: string) => void;
|
|
94
191
|
backend: SlotlockAgentCalendarBackend;
|
|
95
192
|
/**
|
|
96
193
|
* As the agent server's: false refuses an operation. Every offered operation by default. It also
|
|
@@ -135,11 +232,70 @@ export interface SlotlockDashboardAssistantOptions {
|
|
|
135
232
|
/** How long a conversation is kept after its last message: 2 hours by default. */
|
|
136
233
|
conversationTtlMs?: number;
|
|
137
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Hosted Slotlock's plans (plans.ts) and their billing: a Plan page with the tenant's plan and use,
|
|
237
|
+
* Upgrade (to a checkout page) and Manage (to a billing portal). Off unless set.
|
|
238
|
+
*/
|
|
239
|
+
export interface SlotlockDashboardBilling {
|
|
240
|
+
/** The tenant's plan (its own, or the default), or null when it has none (the server's limits apply). */
|
|
241
|
+
plan(tenantRef: string): Promise<SlotlockTenantPlan | null>;
|
|
242
|
+
/**
|
|
243
|
+
* Puts a tenant new to plans on the default plan before it adds a key, resource or webhook
|
|
244
|
+
* endpoint, while the default does not yet apply to every tenant (plans.ts).
|
|
245
|
+
*/
|
|
246
|
+
enrol?(tenantRef: string): Promise<void>;
|
|
247
|
+
/** A checkout URL for the Developer plan, or the Team plan for an organization (with `teamPrices`). */
|
|
248
|
+
checkout(input: {
|
|
249
|
+
tenantRef: string;
|
|
250
|
+
interval: 'month' | 'year';
|
|
251
|
+
plan?: 'developer' | 'team';
|
|
252
|
+
customer: string | null;
|
|
253
|
+
successUrl: string;
|
|
254
|
+
cancelUrl: string;
|
|
255
|
+
}): Promise<string>;
|
|
256
|
+
/** A billing portal URL for the tenant's customer. */
|
|
257
|
+
portal(input: {
|
|
258
|
+
customer: string;
|
|
259
|
+
returnUrl: string;
|
|
260
|
+
}): Promise<string>;
|
|
261
|
+
/** The Developer plan's prices as shown, such as `$10`. */
|
|
262
|
+
prices: {
|
|
263
|
+
month: string;
|
|
264
|
+
year: string;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* The Team plan's prices as shown (Teams on Cloud). With them, an organization's workspace is
|
|
268
|
+
* offered Team instead of Developer.
|
|
269
|
+
*/
|
|
270
|
+
teamPrices?: {
|
|
271
|
+
month: string;
|
|
272
|
+
year: string;
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Metered overage on Developer (SLOTLOCK_OVERAGE, overage.ts): its price as shown, such as `$2
|
|
276
|
+
* per 10,000 operations`, and the percent of the quota where use is refused.
|
|
277
|
+
*/
|
|
278
|
+
overage?: {
|
|
279
|
+
price: string;
|
|
280
|
+
stopPercent: number;
|
|
281
|
+
};
|
|
282
|
+
/** Where a Custom (self-hosted) enquiry goes: an https or mailto URL. */
|
|
283
|
+
contact: string;
|
|
284
|
+
/** The https origins Upgrade and Manage redirect to, allowed in the policy's form-action. */
|
|
285
|
+
formOrigins: readonly string[];
|
|
286
|
+
}
|
|
138
287
|
export interface SlotlockDashboardOptions {
|
|
139
288
|
/** Where people reach the server; the dashboard lives at `<publicUrl>/dashboard`. */
|
|
140
289
|
publicUrl: string;
|
|
141
290
|
/** What the deployment is called in the dashboard: 1-60 characters. Defaults to `Slotlock`. */
|
|
142
291
|
name?: string;
|
|
292
|
+
/**
|
|
293
|
+
* The terms people accept by signing in, an https URL. The sign-in page links them beside the
|
|
294
|
+
* button; without it, the page names no terms.
|
|
295
|
+
*/
|
|
296
|
+
termsUrl?: string;
|
|
297
|
+
/** The privacy notice the sign-in page links, an https URL. */
|
|
298
|
+
privacyUrl?: string;
|
|
143
299
|
/**
|
|
144
300
|
* Where agents reach the server's MCP and A2A endpoints, which the dashboard shows, when it is
|
|
145
301
|
* served from another origin (on Cloudflare Workers, with the server elsewhere). Defaults to
|
|
@@ -158,9 +314,25 @@ export interface SlotlockDashboardOptions {
|
|
|
158
314
|
userUrl?: string;
|
|
159
315
|
membershipsUrl?: string;
|
|
160
316
|
};
|
|
317
|
+
/**
|
|
318
|
+
* Sign-in with Google (OpenID Connect), beside GitHub, for people without a GitHub account: each
|
|
319
|
+
* is their own tenant, `google:<subject>`. Not offered with a fixed list of `organizations`,
|
|
320
|
+
* which are GitHub's. Its redirect URI is the same `<publicUrl>/dashboard/callback`.
|
|
321
|
+
*/
|
|
322
|
+
google?: {
|
|
323
|
+
clientId: string;
|
|
324
|
+
clientSecret: string;
|
|
325
|
+
authorizeUrl?: string;
|
|
326
|
+
tokenUrl?: string;
|
|
327
|
+
/** Google's signing keys (JWKS); https://www.googleapis.com/oauth2/v3/certs by default. */
|
|
328
|
+
jwksUrl?: string;
|
|
329
|
+
};
|
|
161
330
|
/** Seals sessions and sign-in state and derives CSRF tokens: 32+ random characters. */
|
|
162
331
|
sessionSecret: string;
|
|
163
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* GitHub user ids (numbers, as strings) and Google accounts (`google:<subject>`) that may sign in,
|
|
334
|
+
* or `'*'` for every account.
|
|
335
|
+
*/
|
|
164
336
|
allowedUsers: '*' | readonly string[];
|
|
165
337
|
/**
|
|
166
338
|
* GitHub organization ids (numbers, as strings) this dashboard serves. With them, a tenant is an
|
|
@@ -171,8 +343,14 @@ export interface SlotlockDashboardOptions {
|
|
|
171
343
|
* ends access at the next sign-in, at most a session (12 hours) later, and removing an
|
|
172
344
|
* organization from this list signs its sessions out at once. Without it, each person is their
|
|
173
345
|
* own tenant (`github:<id>`).
|
|
346
|
+
*
|
|
347
|
+
* `'enabled'` (Teams on Cloud, with `teams`): each person signs in to their own tenant and may
|
|
348
|
+
* switch to any organization they belong to that an owner turned on, on the Teams page; turning
|
|
349
|
+
* one off ends its use at once.
|
|
174
350
|
*/
|
|
175
|
-
organizations?: readonly string[];
|
|
351
|
+
organizations?: readonly string[] | 'enabled';
|
|
352
|
+
/** The organizations turned on, with `organizations: 'enabled'` (teams.ts). */
|
|
353
|
+
teams?: SlotlockDashboardTeams;
|
|
176
354
|
keys: SlotlockApiKeyStore;
|
|
177
355
|
resources: SlotlockDashboardResources;
|
|
178
356
|
/** Shared by every server behind this origin: see SlotlockDashboardState. */
|
|
@@ -181,6 +359,18 @@ export interface SlotlockDashboardOptions {
|
|
|
181
359
|
availability?: readonly WeeklyAvailabilityRule[];
|
|
182
360
|
/** The chat assistant; off unless set. */
|
|
183
361
|
assistant?: SlotlockDashboardAssistantOptions;
|
|
362
|
+
/** Plans and billing (hosted Slotlock); off unless set. */
|
|
363
|
+
billing?: SlotlockDashboardBilling;
|
|
364
|
+
/**
|
|
365
|
+
* The deployment meters usage (SLOTLOCK_USAGE=on): the Plan page shows this month's, from the
|
|
366
|
+
* resources adapter's `usage`.
|
|
367
|
+
*/
|
|
368
|
+
usage?: boolean;
|
|
369
|
+
/**
|
|
370
|
+
* The deployment holds tenants to their plans' monthly quotas (SLOTLOCK_QUOTAS=on): the Plan page
|
|
371
|
+
* shows this month's use against them, and warns from 80%. Needs `usage`.
|
|
372
|
+
*/
|
|
373
|
+
quotas?: boolean;
|
|
184
374
|
fetch?: typeof fetch;
|
|
185
375
|
now?: () => number;
|
|
186
376
|
/**
|
|
@@ -204,7 +394,13 @@ export interface SlotlockDashboard {
|
|
|
204
394
|
* The dashboard's resources, in the store under each tenant's row-level security. Adds hold the cap
|
|
205
395
|
* in the database (a READ COMMITTED transaction, under a per-tenant lock).
|
|
206
396
|
*/
|
|
207
|
-
export declare function createSlotlockDashboardResources(store: SlotlockStore
|
|
397
|
+
export declare function createSlotlockDashboardResources(store: SlotlockStore, options?: {
|
|
398
|
+
/**
|
|
399
|
+
* The agent backend (`createSlotlockStoreAgentBackend`) a granted approval's booking is made
|
|
400
|
+
* through: with it, the dashboard lists approvals and decides them.
|
|
401
|
+
*/
|
|
402
|
+
agentBackend?: SlotlockAgentCalendarBackend;
|
|
403
|
+
}): SlotlockDashboardResources;
|
|
208
404
|
/**
|
|
209
405
|
* Records accepted write confirmations (`SlotlockAgentConfirmationOptions.consumeAcceptance`) in
|
|
210
406
|
* Postgres, shared by every server on the database: true the first time a confirmation's nonce is
|