slotlock 0.9.0 → 0.10.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/NOTICE +2 -0
  3. package/README.md +4 -2
  4. package/agent-store-backend.js +209 -15
  5. package/application-api.d.ts +10 -1
  6. package/application-api.js +213 -2
  7. package/availability-store.js +13 -5
  8. package/booking-rules.d.ts +11 -0
  9. package/booking-rules.js +21 -1
  10. package/config.d.ts +50 -0
  11. package/config.js +107 -0
  12. package/dashboard-scope.d.ts +7 -0
  13. package/dashboard-scope.js +21 -0
  14. package/dashboard.d.ts +97 -2
  15. package/dashboard.js +3068 -284
  16. package/ddl-identity.d.ts +25 -0
  17. package/ddl-identity.js +881 -0
  18. package/ddl-scim.d.ts +5 -0
  19. package/ddl-scim.js +740 -0
  20. package/ddl-sso.d.ts +15 -0
  21. package/ddl-sso.js +1033 -0
  22. package/ddl.d.ts +40 -9
  23. package/ddl.js +2123 -49
  24. package/domains.d.ts +117 -0
  25. package/domains.js +340 -0
  26. package/event-feed.d.ts +1 -1
  27. package/event-feed.js +6 -34
  28. package/holiday-import.d.ts +1 -1
  29. package/holiday-import.js +2 -3
  30. package/holidays.js +70 -11
  31. package/index.d.ts +12 -4
  32. package/index.js +8 -2
  33. package/invitation-email.d.ts +26 -0
  34. package/invitation-email.js +73 -0
  35. package/maintenance.d.ts +7 -2
  36. package/maintenance.js +15 -1
  37. package/oidc.d.ts +135 -0
  38. package/oidc.js +452 -0
  39. package/openapi.json +1886 -6
  40. package/package.json +2 -1
  41. package/scim-server.d.ts +19 -0
  42. package/scim-server.js +243 -0
  43. package/scim.d.ts +180 -0
  44. package/scim.js +416 -0
  45. package/self-host.d.ts +19 -6
  46. package/self-host.js +262 -7
  47. package/slotlock.manifest.json +1 -1
  48. package/store.d.ts +273 -2
  49. package/store.js +984 -72
  50. package/types.d.ts +33 -1
  51. package/types.js +68 -1
  52. package/workspace-domains.d.ts +146 -0
  53. package/workspace-domains.js +249 -0
  54. package/workspace-roles.d.ts +47 -0
  55. package/workspace-roles.js +304 -0
  56. package/workspace-scim.d.ts +89 -0
  57. package/workspace-scim.js +157 -0
  58. package/workspace-sso.d.ts +131 -0
  59. package/workspace-sso.js +180 -0
  60. package/workspaces.d.ts +240 -0
  61. package/workspaces.js +433 -0
@@ -538,12 +538,20 @@ export async function deleteHolidayCalendar(sql, params) {
538
538
  }
539
539
  /** Subscriptions due to be imported again, the longest waiting first. */
540
540
  export async function dueHolidayCalendars(sql, params) {
541
+ // Each is put off as a failure would put it off before it is read, so a pass that never finishes
542
+ // one (a process stopped on it) does not try it again at once, and two passes do not both read it.
543
+ // Importing it sets its next import; a failure says why.
541
544
  const rows = await sql `
542
- SELECT ${sql.unsafe(HOLIDAY_CALENDAR_COLUMNS)} FROM slotlock.holiday_calendars calendar
543
- WHERE calendar.tenant_ref = ${params.tenantRef} AND calendar.next_import_at <= statement_timestamp()
544
- ORDER BY calendar.next_import_at, calendar.id
545
- LIMIT ${params.limit}`;
546
- return rows.map(toHolidayCalendar);
545
+ UPDATE slotlock.holiday_calendars calendar
546
+ SET next_import_at = statement_timestamp() + make_interval(hours => ${SLOTLOCK_HOLIDAY_RETRY_HOURS})
547
+ WHERE calendar.id IN (
548
+ SELECT id FROM slotlock.holiday_calendars
549
+ WHERE tenant_ref = ${params.tenantRef} AND next_import_at <= statement_timestamp()
550
+ ORDER BY next_import_at, id
551
+ LIMIT ${params.limit}
552
+ FOR UPDATE SKIP LOCKED)
553
+ RETURNING ${sql.unsafe(HOLIDAY_CALENDAR_COLUMNS)}`;
554
+ return rows.map(toHolidayCalendar).sort((left, right) => left.id.localeCompare(right.id));
547
555
  }
548
556
  /** Note why a subscription's import failed, and try again in SLOTLOCK_HOLIDAY_RETRY_HOURS. */
549
557
  export async function noteHolidayCalendarFailure(sql, params) {
@@ -65,6 +65,12 @@ export interface SlotlockBookingRules {
65
65
  * money. With `withinMinutes`, only a cancellation that late before the start.
66
66
  */
67
67
  cancellationFee?: SlotlockCancellationFee;
68
+ /**
69
+ * What holding the resource costs, set by the tenant (RFC 0004 C3): an agent's hold of it waits for
70
+ * this payment (at most 30 minutes) whatever the agent asks, an agent names no other price, and an
71
+ * agent books it only from such a hold, once paid. The tenant's own systems set their own.
72
+ */
73
+ holdPrice?: SlotlockHoldPrice;
68
74
  /** A booking lies inside the resource's bookable hours. */
69
75
  enforceHours?: boolean;
70
76
  /**
@@ -87,6 +93,11 @@ export interface SlotlockCancellationFee {
87
93
  /** Only a cancellation this many minutes or fewer before the start: 0-525,600. */
88
94
  withinMinutes?: number;
89
95
  }
96
+ /** A hold's price: an amount in the currency's minor unit (cents), ISO 4217 currency. */
97
+ export interface SlotlockHoldPrice {
98
+ amountMinor: number;
99
+ currency: string;
100
+ }
90
101
  /** A window of a day: minutes after local midnight, and how long (it may run past midnight). */
91
102
  export interface SlotlockDayWindow {
92
103
  startMinutes: number;
package/booking-rules.js CHANGED
@@ -22,7 +22,7 @@ const LIMITS = SLOTLOCK_BOOKING_RULE_LIMITS;
22
22
  export const SLOTLOCK_MAX_ALLOWED_DURATIONS = 20;
23
23
  /** Windows `pickupTimes` and `returnTimes` may each hold. */
24
24
  export const SLOTLOCK_MAX_DAY_WINDOWS = 4;
25
- const LIST_RULES = new Set(['allowedDurations', 'pickupTimes', 'returnTimes', 'cancellationFee']);
25
+ const LIST_RULES = new Set(['allowedDurations', 'pickupTimes', 'returnTimes', 'cancellationFee', 'holdPrice']);
26
26
  /** The `hoursApply` a resource's rules mean: as given, else `ends` with pick-up or return times. */
27
27
  export function hoursApplyOf(rules) {
28
28
  if (rules?.hoursApply !== undefined)
@@ -34,6 +34,22 @@ function parseHoursApply(value) {
34
34
  throw new TypeError("Slotlock booking rule hoursApply must be 'whole' or 'ends'");
35
35
  return value;
36
36
  }
37
+ /** `holdPrice` from untrusted input, or a TypeError. */
38
+ function parseHoldPrice(value) {
39
+ const price = value;
40
+ if (typeof price !== 'object' ||
41
+ price === null ||
42
+ Array.isArray(price) ||
43
+ Object.keys(price).some((key) => !['amountMinor', 'currency'].includes(key)) ||
44
+ !Number.isSafeInteger(price.amountMinor) ||
45
+ price.amountMinor < 1 ||
46
+ price.amountMinor > 1_000_000_000_000 ||
47
+ typeof price.currency !== 'string' ||
48
+ !/^[A-Z]{3}$/.test(price.currency)) {
49
+ throw new TypeError('Slotlock booking rule holdPrice needs amountMinor (a whole 1-1,000,000,000,000) and currency (ISO 4217, like EUR)');
50
+ }
51
+ return { amountMinor: price.amountMinor, currency: price.currency };
52
+ }
37
53
  /** `cancellationFee` from untrusted input, or a TypeError. */
38
54
  function parseCancellationFee(value) {
39
55
  const fee = value;
@@ -152,6 +168,8 @@ export function parseBookingRules(value) {
152
168
  rules.allowedDurations = parseAllowedDurations(input.allowedDurations);
153
169
  if (input.cancellationFee !== undefined)
154
170
  rules.cancellationFee = parseCancellationFee(input.cancellationFee);
171
+ if (input.holdPrice !== undefined)
172
+ rules.holdPrice = parseHoldPrice(input.holdPrice);
155
173
  if (input.pickupTimes !== undefined)
156
174
  rules.pickupTimes = parseDayWindows('pickupTimes', input.pickupTimes);
157
175
  if (input.returnTimes !== undefined)
@@ -218,6 +236,8 @@ export function parseEffectiveBookingRules(value) {
218
236
  rules.allowedDurations = parseAllowedDurations(field);
219
237
  else if (key === 'cancellationFee')
220
238
  rules.cancellationFee = parseCancellationFee(field);
239
+ else if (key === 'holdPrice')
240
+ rules.holdPrice = parseHoldPrice(field);
221
241
  else
222
242
  rules[key] = parseDayWindows(key, field);
223
243
  }
package/config.d.ts CHANGED
@@ -42,6 +42,41 @@ export interface SlotlockServeDashboardConfig {
42
42
  * `'enabled'`: those an owner turned on from the dashboard (Teams on Cloud, teams.ts).
43
43
  */
44
44
  organizations?: string[] | 'enabled';
45
+ /**
46
+ * Team workspaces (SLOTLOCK_DASHBOARD_TEAM_WORKSPACES=on): anyone signed in creates one and invites
47
+ * people to it by email, each with a role (workspaces.ts).
48
+ */
49
+ teamWorkspaces?: boolean;
50
+ /**
51
+ * Team invitations sent by email (SLOTLOCK_INVITATION_EMAIL resend or postmark, with
52
+ * SLOTLOCK_INVITATION_EMAIL_KEY and SLOTLOCK_INVITATION_EMAIL_FROM); without it, the person inviting
53
+ * sends the link.
54
+ */
55
+ invitationEmail?: {
56
+ provider: 'resend' | 'postmark';
57
+ key: string;
58
+ from: string;
59
+ };
60
+ /**
61
+ * Verified domains (SLOTLOCK_VERIFIED_DOMAINS=on, with team workspaces): a team's Owner proves a
62
+ * domain with a DNS record, and the people whose verified address is at it may ask to join. Looked
63
+ * up over DNS-over-HTTPS, or with the system's resolver (SLOTLOCK_DNS_RESOLVER=system, a server only).
64
+ */
65
+ verifiedDomains?: {
66
+ resolver: 'doh' | 'system';
67
+ };
68
+ /**
69
+ * Single sign-on through each team's own identity provider (SLOTLOCK_TEAM_SSO=on, with verified
70
+ * domains): its client secrets sealed under SLOTLOCK_SSO_SECRET.
71
+ */
72
+ teamSso?: {
73
+ secret: string;
74
+ };
75
+ /**
76
+ * SCIM provisioning (SLOTLOCK_TEAM_SCIM=on, with verified domains): each team's identity provider
77
+ * adds, deactivates and groups its people at the dashboard's /scim/v2.
78
+ */
79
+ teamScim?: boolean;
45
80
  /** What the deployment is called in the dashboard (SLOTLOCK_DASHBOARD_NAME). */
46
81
  name?: string;
47
82
  /**
@@ -85,6 +120,21 @@ export declare function readDashboardConfig(env: SlotlockEnv): SlotlockServeDash
85
120
  * organizations' keys, and only theirs.
86
121
  */
87
122
  export declare function readDashboardOrganizations(env: SlotlockEnv): string[] | 'enabled' | undefined;
123
+ /**
124
+ * SLOTLOCK_VERIFIED_DOMAINS: on or off (the default), only with team workspaces; and
125
+ * SLOTLOCK_DNS_RESOLVER, how a domain's record is looked up: doh (the default: Cloudflare's
126
+ * DNS-over-HTTPS, cross-checked by Google's) or system (the server's own resolver, which follows
127
+ * split-horizon DNS). Read on its own as well: the server that runs maintenance checks the domains
128
+ * each day, wherever the dashboard runs.
129
+ */
130
+ export declare function readVerifiedDomains(env: SlotlockEnv): {
131
+ resolver: 'doh' | 'system';
132
+ } | undefined;
133
+ /**
134
+ * SLOTLOCK_DASHBOARD_TEAM_WORKSPACES: on or off (the default). Read on its own as well: a server whose
135
+ * dashboard runs elsewhere (on Cloudflare Workers) takes team workspaces' keys only with it on.
136
+ */
137
+ export declare function readDashboardTeamWorkspaces(env: SlotlockEnv): boolean;
88
138
  /** A token bucket: `perSecond` tokens come back each second, and it holds at most `burst`. */
89
139
  export interface SlotlockRate {
90
140
  perSecond: number;
package/config.js CHANGED
@@ -181,6 +181,14 @@ export function readDashboardConfig(env) {
181
181
  if (Array.isArray(orgs) && googleClientId !== undefined) {
182
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
183
  }
184
+ const teamWorkspaces = readDashboardTeamWorkspaces(env);
185
+ if (teamWorkspaces && Array.isArray(orgs)) {
186
+ throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_TEAM_WORKSPACES does not go with a list of SLOTLOCK_DASHBOARD_ORGS: with a list, everyone works in those organizations');
187
+ }
188
+ const invitationEmail = readInvitationEmail(env, teamWorkspaces);
189
+ const verifiedDomains = readVerifiedDomains(env);
190
+ const teamSso = readTeamSso(env, verifiedDomains !== undefined);
191
+ const teamScim = readTeamScim(env, verifiedDomains !== undefined);
184
192
  const name = optional(env, 'SLOTLOCK_DASHBOARD_NAME')?.trim();
185
193
  if (name !== undefined && name !== '' && !SLOTLOCK_DASHBOARD_NAME_PATTERN.test(name)) {
186
194
  throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_NAME must be 1-60 characters, without <, > or control or invisible characters');
@@ -196,6 +204,11 @@ export function readDashboardConfig(env) {
196
204
  ? { google: { clientId: googleClientId, clientSecret: googleClientSecret } }
197
205
  : {}),
198
206
  ...(orgs !== undefined ? { organizations: orgs } : {}),
207
+ ...(teamWorkspaces ? { teamWorkspaces: true } : {}),
208
+ ...(invitationEmail ? { invitationEmail } : {}),
209
+ ...(verifiedDomains ? { verifiedDomains } : {}),
210
+ ...(teamSso ? { teamSso } : {}),
211
+ ...(teamScim ? { teamScim } : {}),
199
212
  ...(name !== undefined && name !== '' ? { name } : {}),
200
213
  ...(termsUrl !== undefined ? { termsUrl } : {}),
201
214
  ...(privacyUrl !== undefined ? { privacyUrl } : {}),
@@ -265,6 +278,100 @@ export function readDashboardOrganizations(env) {
265
278
  }
266
279
  return [...new Set(orgs.split(','))];
267
280
  }
281
+ /**
282
+ * SLOTLOCK_INVITATION_EMAIL (resend or postmark), with its key and the verified sender: all three, or
283
+ * none. Only with team workspaces, the one thing it sends.
284
+ */
285
+ function readInvitationEmail(env, teamWorkspaces) {
286
+ const provider = optional(env, 'SLOTLOCK_INVITATION_EMAIL');
287
+ const key = optional(env, 'SLOTLOCK_INVITATION_EMAIL_KEY');
288
+ const from = optional(env, 'SLOTLOCK_INVITATION_EMAIL_FROM');
289
+ if (provider === undefined && key === undefined && from === undefined)
290
+ return undefined;
291
+ if (provider !== 'resend' && provider !== 'postmark') {
292
+ throw new SlotlockConfigError('SLOTLOCK_INVITATION_EMAIL must be resend or postmark');
293
+ }
294
+ if (!teamWorkspaces) {
295
+ throw new SlotlockConfigError('SLOTLOCK_INVITATION_EMAIL sends team invitations: it needs SLOTLOCK_DASHBOARD_TEAM_WORKSPACES=on');
296
+ }
297
+ if (key === undefined || !/^[\x21-\x7e]{10,500}$/.test(key)) {
298
+ throw new SlotlockConfigError(`SLOTLOCK_INVITATION_EMAIL_KEY must be ${provider === 'resend' ? "Resend's API key" : "Postmark's server token"}`);
299
+ }
300
+ if (from === undefined || !/^([^<>\r\n"]{1,80} )?<?[^@\s<>"]{1,64}@[^@\s<>"]+\.[^@\s<>"]+>?$/.test(from)) {
301
+ throw new SlotlockConfigError('SLOTLOCK_INVITATION_EMAIL_FROM must be the sender the provider verified: team@example.com or Name <team@example.com>');
302
+ }
303
+ return { provider, key, from };
304
+ }
305
+ /**
306
+ * SLOTLOCK_VERIFIED_DOMAINS: on or off (the default), only with team workspaces; and
307
+ * SLOTLOCK_DNS_RESOLVER, how a domain's record is looked up: doh (the default: Cloudflare's
308
+ * DNS-over-HTTPS, cross-checked by Google's) or system (the server's own resolver, which follows
309
+ * split-horizon DNS). Read on its own as well: the server that runs maintenance checks the domains
310
+ * each day, wherever the dashboard runs.
311
+ */
312
+ export function readVerifiedDomains(env) {
313
+ const value = optional(env, 'SLOTLOCK_VERIFIED_DOMAINS') ?? 'off';
314
+ if (value !== 'on' && value !== 'off') {
315
+ throw new SlotlockConfigError('SLOTLOCK_VERIFIED_DOMAINS must be on or off');
316
+ }
317
+ const resolver = optional(env, 'SLOTLOCK_DNS_RESOLVER');
318
+ if (resolver !== undefined && resolver !== 'doh' && resolver !== 'system') {
319
+ throw new SlotlockConfigError('SLOTLOCK_DNS_RESOLVER must be doh or system');
320
+ }
321
+ if (value === 'off') {
322
+ if (resolver !== undefined) {
323
+ throw new SlotlockConfigError('SLOTLOCK_DNS_RESOLVER looks up verified domains: it needs SLOTLOCK_VERIFIED_DOMAINS=on');
324
+ }
325
+ return undefined;
326
+ }
327
+ if (!readDashboardTeamWorkspaces(env)) {
328
+ throw new SlotlockConfigError('SLOTLOCK_VERIFIED_DOMAINS verifies team workspaces\' domains: it needs SLOTLOCK_DASHBOARD_TEAM_WORKSPACES=on');
329
+ }
330
+ return { resolver: resolver ?? 'doh' };
331
+ }
332
+ /**
333
+ * SLOTLOCK_TEAM_SSO: on or off (the default), only with verified domains; with it,
334
+ * SLOTLOCK_SSO_SECRET (32+ characters), which seals each team's client secret.
335
+ */
336
+ function readTeamSso(env, verifiedDomains) {
337
+ const value = optional(env, 'SLOTLOCK_TEAM_SSO') ?? 'off';
338
+ if (value !== 'on' && value !== 'off')
339
+ throw new SlotlockConfigError('SLOTLOCK_TEAM_SSO must be on or off');
340
+ const secret = optional(env, 'SLOTLOCK_SSO_SECRET');
341
+ if (value === 'off') {
342
+ if (secret !== undefined)
343
+ throw new SlotlockConfigError('SLOTLOCK_SSO_SECRET seals teams\' client secrets: it needs SLOTLOCK_TEAM_SSO=on');
344
+ return undefined;
345
+ }
346
+ if (!verifiedDomains) {
347
+ throw new SlotlockConfigError('SLOTLOCK_TEAM_SSO trusts addresses only at verified domains: it needs SLOTLOCK_VERIFIED_DOMAINS=on');
348
+ }
349
+ if (secret === undefined || secret.length < 32 || /\s/.test(secret)) {
350
+ throw new SlotlockConfigError(`SLOTLOCK_SSO_SECRET must be 32 or more characters without spaces. ${SECRET_HINT}`);
351
+ }
352
+ return { secret };
353
+ }
354
+ /** SLOTLOCK_TEAM_SCIM: on or off (the default), only with verified domains. */
355
+ function readTeamScim(env, verifiedDomains) {
356
+ const value = optional(env, 'SLOTLOCK_TEAM_SCIM') ?? 'off';
357
+ if (value !== 'on' && value !== 'off')
358
+ throw new SlotlockConfigError('SLOTLOCK_TEAM_SCIM must be on or off');
359
+ if (value === 'on' && !verifiedDomains) {
360
+ throw new SlotlockConfigError('SLOTLOCK_TEAM_SCIM adds people only at verified domains: it needs SLOTLOCK_VERIFIED_DOMAINS=on');
361
+ }
362
+ return value === 'on';
363
+ }
364
+ /**
365
+ * SLOTLOCK_DASHBOARD_TEAM_WORKSPACES: on or off (the default). Read on its own as well: a server whose
366
+ * dashboard runs elsewhere (on Cloudflare Workers) takes team workspaces' keys only with it on.
367
+ */
368
+ export function readDashboardTeamWorkspaces(env) {
369
+ const value = optional(env, 'SLOTLOCK_DASHBOARD_TEAM_WORKSPACES') ?? 'off';
370
+ if (value !== 'on' && value !== 'off') {
371
+ throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_TEAM_WORKSPACES must be on or off');
372
+ }
373
+ return value === 'on';
374
+ }
268
375
  /** The token buckets `slotlock serve` keeps (SLOTLOCK_LIMITS), each counted per its own key. */
269
376
  export const SLOTLOCK_RATE_NAMES = Object.freeze([
270
377
  'address',
@@ -0,0 +1,7 @@
1
+ import type { SlotlockResourceScope } from './resource-profile.js';
2
+ /** Serve one dashboard request with a scope of its own, none until the person's is read. */
3
+ export declare function runSlotlockDashboardRequest<T>(serve: () => T): T;
4
+ /** The request's person is limited to `scope`: what is read for them from now on keeps to it. */
5
+ export declare function setSlotlockDashboardRequestScope(scope: SlotlockResourceScope | null): void;
6
+ /** The scope of the dashboard request being served, or null for none (or outside a request). */
7
+ export declare function slotlockDashboardRequestScope(): SlotlockResourceScope | null;
@@ -0,0 +1,21 @@
1
+ // The locations and groups the person signed in to the dashboard is limited to (RFC 0004 D1), for
2
+ // the request being served. The dashboard sets it once it has read the person's standing; the
3
+ // store its resources read through (createSlotlockDashboardResources) runs each transaction with it
4
+ // as its resource scope, so the database's resource_scope_v1 policies keep every read and write to
5
+ // those resources, as they do for a key limited to them.
6
+ import { AsyncLocalStorage } from 'node:async_hooks';
7
+ const requests = new AsyncLocalStorage();
8
+ /** Serve one dashboard request with a scope of its own, none until the person's is read. */
9
+ export function runSlotlockDashboardRequest(serve) {
10
+ return requests.run({ scope: null }, serve);
11
+ }
12
+ /** The request's person is limited to `scope`: what is read for them from now on keeps to it. */
13
+ export function setSlotlockDashboardRequestScope(scope) {
14
+ const request = requests.getStore();
15
+ if (request)
16
+ request.scope = scope;
17
+ }
18
+ /** The scope of the dashboard request being served, or null for none (or outside a request). */
19
+ export function slotlockDashboardRequestScope() {
20
+ return requests.getStore()?.scope ?? null;
21
+ }
package/dashboard.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import type { SlotlockDashboardTeams } from './teams.js';
2
+ import type { SlotlockDashboardWorkspaces } from './workspaces.js';
3
+ import { type SlotlockDnsResolver } from './domains.js';
4
+ import { type SlotlockSecretBox } from './oidc.js';
5
+ import type { SlotlockAuditEvent, SlotlockAuditEventInput } from './store.js';
2
6
  import { type SlotlockApiKeyStore } from './api-keys.js';
3
7
  import { type SlotlockAgentCalendarBackend, type SlotlockAgentOperation, type SlotlockAgentOperationDispatchOptions, type SlotlockAgentPrincipal, type SlotlockAgentUsage } from './agent-server.js';
4
8
  import { type SlotlockAssistantStore } from './assistant.js';
@@ -8,7 +12,7 @@ import { type SlotlockPlan, type SlotlockTenantPlan } from './plans.js';
8
12
  import { type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceProfileInput } from './resource-profile.js';
9
13
  import { type SlotlockUsageRow } from './usage.js';
10
14
  import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
11
- import type { SlotlockSql, SlotlockStore, SlotlockWaitlistEntry } from './store.js';
15
+ import { type SlotlockSql, type SlotlockStore, type SlotlockSubject, type SlotlockSubjectErasure, type SlotlockSubjectExport, type SlotlockTenantExport, type SlotlockTenantExportPartResult, type SlotlockWaitlistEntry } from './store.js';
12
16
  import type { SlotlockAvailabilityDefaults, SlotlockAvailabilityExceptionInput, SlotlockHolidayCalendar, SlotlockOverrideResources } from './availability-store.js';
13
17
  import { type SlotlockAvailabilityException } from './hours.js';
14
18
  import { type SlotlockHolidaySource } from './holiday-import.js';
@@ -21,6 +25,12 @@ export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 500;
21
25
  * person who made it (`github:<user id>`) as `actor`, to record on the event feed.
22
26
  */
23
27
  export interface SlotlockDashboardResources {
28
+ /**
29
+ * These adapters keep each request to the locations and groups its member is limited to (RFC
30
+ * 0004 D1), as createSlotlockDashboardResources's do (dashboard-scope.ts). Without it, a limited
31
+ * member reaches only their own account.
32
+ */
33
+ keepsToScope?: boolean;
24
34
  list(tenantRef: string): Promise<SlotlockResource[]>;
25
35
  /** How many resources the tenant has, archived ones included, as a resource limit counts them. */
26
36
  count?(tenantRef: string): Promise<number>;
@@ -45,6 +55,19 @@ export interface SlotlockDashboardResources {
45
55
  } | {
46
56
  location: string;
47
57
  } | 'all', rules: WeeklyAvailabilityRule[] | null, actor?: string, override?: SlotlockOverrideResources): Promise<number>;
58
+ /**
59
+ * What the tenant keeps, exported whole, and what it keeps about one person, exported or erased
60
+ * (RFC 0004 D3): Settings, Privacy.
61
+ */
62
+ privacy?: {
63
+ /** Ask for an export, built in the background; refused while one is built, or with five kept. */
64
+ createExport(tenantRef: string, actor?: string): Promise<SlotlockTenantExport>;
65
+ listExports(tenantRef: string): Promise<SlotlockTenantExport[]>;
66
+ exportPart(tenantRef: string, exportId: string, part: number): Promise<SlotlockTenantExportPartResult>;
67
+ deleteExport(tenantRef: string, exportId: string): Promise<boolean>;
68
+ exportSubject(tenantRef: string, subject: SlotlockSubject): Promise<SlotlockSubjectExport>;
69
+ eraseSubject(tenantRef: string, subject: SlotlockSubject, actor?: string): Promise<SlotlockSubjectErasure>;
70
+ };
48
71
  /** The people the tenant keeps to tell about bookings (RFC 0004 P3), and forgetting one. */
49
72
  contacts?: {
50
73
  list(tenantRef: string, limit: number, after?: string): Promise<SlotlockContact[]>;
@@ -174,6 +197,27 @@ export interface SlotlockDashboardResources {
174
197
  };
175
198
  /** The tenant's assistant settings and its members' conversations (assistant.ts). */
176
199
  assistant?: SlotlockAssistantStore;
200
+ /**
201
+ * The tenant's audit log (slotlock.audit_events): who changed keys, webhooks, billing and
202
+ * settings, and every refusal. Append-only: record, and read newest first.
203
+ */
204
+ audit?: {
205
+ /** Whether the workspace's chain holds from its oldest event kept (store verifyAuditChain). */
206
+ verify?(tenantRef: string): Promise<{
207
+ checked: number;
208
+ brokenAt: number | null;
209
+ }>;
210
+ record(tenantRef: string, event: SlotlockAuditEventInput): Promise<void>;
211
+ list(tenantRef: string, params?: {
212
+ limit?: number;
213
+ before?: {
214
+ occurredAt: Date;
215
+ id: string;
216
+ };
217
+ action?: string;
218
+ actorRef?: string;
219
+ }): Promise<SlotlockAuditEvent[]>;
220
+ };
177
221
  /** The tenant's webhook endpoints, as the event feed keeps them. */
178
222
  webhooks?: {
179
223
  list(tenantRef: string): Promise<SlotlockWebhookEndpoint[]>;
@@ -350,6 +394,18 @@ export interface SlotlockDashboardBilling {
350
394
  /** The https origins Upgrade and Manage redirect to, allowed in the policy's form-action. */
351
395
  formOrigins: readonly string[];
352
396
  }
397
+ /** Sends an invitation's link by email (hosted Slotlock); without one, the person inviting sends it. */
398
+ export interface SlotlockInvitationSender {
399
+ send(invitation: {
400
+ to: string;
401
+ /** The team workspace's name, and how the person inviting is shown. */
402
+ workspace: string;
403
+ inviter: string;
404
+ role: string;
405
+ link: string;
406
+ expiresAt: Date;
407
+ }): Promise<void>;
408
+ }
353
409
  export interface SlotlockDashboardOptions {
354
410
  /** Where people reach the server; the dashboard lives at `<publicUrl>/dashboard`. */
355
411
  publicUrl: string;
@@ -417,6 +473,45 @@ export interface SlotlockDashboardOptions {
417
473
  organizations?: readonly string[] | 'enabled';
418
474
  /** The organizations turned on, with `organizations: 'enabled'` (teams.ts). */
419
475
  teams?: SlotlockDashboardTeams;
476
+ /**
477
+ * Who belongs to which workspace, and as what (workspaces.ts, workspace-roles.ts): read on every
478
+ * request, so a role changed or a membership ended applies at the next one, and "Sign out
479
+ * everywhere" ends every session. Without it, a person is the Owner of their own workspace, and an
480
+ * organization's GitHub owners are its Owners and its other members Developers, as read at sign-in.
481
+ */
482
+ workspaces?: SlotlockDashboardWorkspaces;
483
+ /**
484
+ * Team workspaces (`team:<id>`): anyone signed in creates one, invites people to it by email with
485
+ * a role, and changes or removes them (workspaces.ts teams). Needs `workspaces` with its
486
+ * database, and no fixed list of organizations. Off unless set.
487
+ */
488
+ teamWorkspaces?: boolean;
489
+ /**
490
+ * Verified domains (workspace-domains.ts): a team's Owner proves a domain with a DNS record, and
491
+ * the people whose verified address is at it may ask to join. Needs team workspaces. The resolvers
492
+ * look the record up when an Owner asks (domains.ts slotlockDomainResolvers); the daily check runs
493
+ * in the server's maintenance. Off unless set.
494
+ */
495
+ domains?: {
496
+ resolvers: readonly SlotlockDnsResolver[];
497
+ };
498
+ /**
499
+ * Single sign-on through each team's own identity provider (OpenID Connect; workspace-sso.ts,
500
+ * oidc.ts): needs verified domains. `secretBox` seals the teams' client secrets; `fetch` reaches
501
+ * the URLs a team's Owner gives (on Node, one that refuses private addresses). Off unless set.
502
+ */
503
+ sso?: {
504
+ secretBox: SlotlockSecretBox;
505
+ fetch?: typeof fetch;
506
+ };
507
+ /**
508
+ * SCIM 2.0 provisioning (workspace-scim.ts, scim-server.ts): each team's identity provider adds,
509
+ * deactivates and groups its people at `<dashboard>/scim/v2/<team id>`, with a token an Owner
510
+ * makes on Settings, Identity. Needs verified domains. Off unless set.
511
+ */
512
+ scim?: boolean;
513
+ /** Sends invitations by email; the person inviting is shown each link either way. */
514
+ invitations?: SlotlockInvitationSender;
420
515
  keys: SlotlockApiKeyStore;
421
516
  resources: SlotlockDashboardResources;
422
517
  /** Shared by every server behind this origin: see SlotlockDashboardState. */
@@ -462,7 +557,7 @@ export interface SlotlockDashboard {
462
557
  * The dashboard's resources, in the store under each tenant's row-level security. Adds hold the cap
463
558
  * in the database (a READ COMMITTED transaction, under a per-tenant lock).
464
559
  */
465
- export declare function createSlotlockDashboardResources(store: SlotlockStore, options?: {
560
+ export declare function createSlotlockDashboardResources(given: SlotlockStore, options?: {
466
561
  /**
467
562
  * The agent backend (`createSlotlockStoreAgentBackend`) a granted approval's booking is made
468
563
  * through: with it, the dashboard lists approvals and decides them.