slotlock 0.8.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 (86) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/NOTICE +2 -0
  3. package/README.md +4 -2
  4. package/agent-server.d.ts +25 -5
  5. package/agent-server.js +152 -7
  6. package/agent-store-backend.d.ts +6 -1
  7. package/agent-store-backend.js +1068 -68
  8. package/application-api.d.ts +28 -1
  9. package/application-api.js +789 -21
  10. package/approvals.d.ts +2 -0
  11. package/approvals.js +12 -5
  12. package/assistant.d.ts +34 -8
  13. package/assistant.js +81 -26
  14. package/availability-store.d.ts +193 -0
  15. package/availability-store.js +566 -0
  16. package/booking-rules.d.ts +103 -5
  17. package/booking-rules.js +263 -15
  18. package/config.d.ts +50 -0
  19. package/config.js +107 -0
  20. package/dashboard-assistant-ui.d.ts +1 -1
  21. package/dashboard-assistant-ui.js +109 -8
  22. package/dashboard-scope.d.ts +7 -0
  23. package/dashboard-scope.js +21 -0
  24. package/dashboard.d.ts +152 -8
  25. package/dashboard.js +3868 -399
  26. package/ddl-identity.d.ts +25 -0
  27. package/ddl-identity.js +881 -0
  28. package/ddl-scim.d.ts +5 -0
  29. package/ddl-scim.js +740 -0
  30. package/ddl-sso.d.ts +15 -0
  31. package/ddl-sso.js +1033 -0
  32. package/ddl.d.ts +44 -9
  33. package/ddl.js +2872 -47
  34. package/domains.d.ts +117 -0
  35. package/domains.js +340 -0
  36. package/event-feed.d.ts +2 -2
  37. package/event-feed.js +24 -34
  38. package/holiday-import.d.ts +38 -0
  39. package/holiday-import.js +56 -0
  40. package/holidays.d.ts +38 -0
  41. package/holidays.js +209 -0
  42. package/hours.d.ts +125 -0
  43. package/hours.js +224 -0
  44. package/index.d.ts +24 -5
  45. package/index.js +17 -3
  46. package/interop.d.ts +25 -7
  47. package/interop.js +145 -17
  48. package/invitation-email.d.ts +26 -0
  49. package/invitation-email.js +73 -0
  50. package/lifecycle.d.ts +31 -0
  51. package/lifecycle.js +115 -0
  52. package/maintenance.d.ts +26 -2
  53. package/maintenance.js +70 -2
  54. package/metrics.js +3 -0
  55. package/oidc.d.ts +135 -0
  56. package/oidc.js +452 -0
  57. package/openapi.json +20172 -9850
  58. package/package.json +2 -1
  59. package/ranges.d.ts +28 -0
  60. package/ranges.js +135 -0
  61. package/resource-search.d.ts +19 -0
  62. package/resource-search.js +153 -0
  63. package/rest-api.js +7 -0
  64. package/rules.d.ts +2 -0
  65. package/rules.js +2 -0
  66. package/scim-server.d.ts +19 -0
  67. package/scim-server.js +243 -0
  68. package/scim.d.ts +180 -0
  69. package/scim.js +416 -0
  70. package/self-host.d.ts +19 -6
  71. package/self-host.js +483 -28
  72. package/slotlock.manifest.json +1 -1
  73. package/store.d.ts +637 -23
  74. package/store.js +2529 -147
  75. package/types.d.ts +230 -0
  76. package/types.js +68 -1
  77. package/workspace-domains.d.ts +146 -0
  78. package/workspace-domains.js +249 -0
  79. package/workspace-roles.d.ts +47 -0
  80. package/workspace-roles.js +304 -0
  81. package/workspace-scim.d.ts +89 -0
  82. package/workspace-scim.js +157 -0
  83. package/workspace-sso.d.ts +131 -0
  84. package/workspace-sso.js +180 -0
  85. package/workspaces.d.ts +240 -0
  86. package/workspaces.js +433 -0
package/index.js CHANGED
@@ -11,7 +11,15 @@ export { createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotloc
11
11
  export { createSlotlockMetrics } from './metrics.js';
12
12
  export { expandRules } from './rules.js';
13
13
  export { availabilityRuleProblem, parseAvailabilityRules, rulesToWeeklyHours, SLOTLOCK_MAX_AVAILABILITY_RULES, weeklyHoursToRules, } from './weekly-hours.js';
14
- export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS, toJSCalendarEvent, } from './interop.js';
14
+ export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS, SLOTLOCK_MAX_VAVAILABILITY_DAYS, toJSCalendarEvent, } from './interop.js';
15
+ // Stored hours that inherit, and closures and special days (RFC 0004 B1, B2).
16
+ export { applicableHours, coveredByOpenWindows, exceptionDays, exceptionRanges, openWindows, parseExceptionDays, SLOTLOCK_MAX_AVAILABILITY_EXCEPTIONS, SLOTLOCK_MAX_EXCEPTION_DAYS, SLOTLOCK_MAX_EXCEPTION_WINDOWS, } from './hours.js';
17
+ export { SLOTLOCK_OVERRIDE_RESOURCES, SLOTLOCK_HOLIDAY_REIMPORT_DAYS, SLOTLOCK_MAX_HOLIDAY_CALENDARS, } from './availability-store.js';
18
+ export { readSlotlockHolidayCalendar, } from './holiday-import.js';
19
+ export { fetchHolidayCalendar, parseHolidayCalendar, SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES, SLOTLOCK_MAX_HOLIDAY_DAYS, } from './holidays.js';
20
+ // The booking lifecycle, written down (RFC 0004 B4).
21
+ export { SLOTLOCK_BOOKING_ACTIONS, SLOTLOCK_BOOKING_ENDS, SLOTLOCK_BOOKING_STATUSES, SLOTLOCK_BOOKING_TRANSITIONS, slotlockBookingNext, slotlockBookingRefusal, } from './lifecycle.js';
22
+ export { findEarliestRange, sameTimeDaysLater } from './ranges.js';
15
23
  export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
16
24
  export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MAX_ALTERNATIVES, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
17
25
  export { createSlotlockNodeServer } from './node-server.js';
@@ -26,8 +34,14 @@ export { createSlotlockPartnerKeyAuthenticator, createSlotlockPartnerStore, pars
26
34
  export { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from './teams.js';
27
35
  export { reportSlotlockOverage, SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT } from './overage.js';
28
36
  export { createSlotlockOtlpExporter, readSlotlockOtlpConfig, slotlockCallSpan } from './otel.js';
37
+ export { createSlotlockInvitationEmailSender, SlotlockInvitationEmailError, slotlockInvitationEmail, } from './invitation-email.js';
38
+ export { createSlotlockDashboardWorkspaces, createSlotlockMemoryDashboardWorkspaces, createSlotlockWorkspaceCheck, } from './workspaces.js';
39
+ export { SLOTLOCK_DASHBOARD_PAGE_CAPABILITIES, SLOTLOCK_DASHBOARD_POST_CAPABILITIES, SLOTLOCK_FRESH_CAPABILITIES, SLOTLOCK_ROLE_CAPABILITIES, SLOTLOCK_SCOPED_CAPABILITIES, SLOTLOCK_SCOPED_DASHBOARD_PAGES, SLOTLOCK_SCOPED_DASHBOARD_POSTS, SLOTLOCK_WORKSPACE_CAPABILITIES, SLOTLOCK_WORKSPACE_ROLE_NAMES, SLOTLOCK_WORKSPACE_ROLES, isSlotlockWorkspaceRole, slotlockMemberCan, slotlockRoleCan, slotlockRoleKeyScopes, slotlockRoleMayGrant, } from './workspace-roles.js';
40
+ // For resource adapters of one's own that keep to a limited member's scope (`keepsToScope`).
41
+ export { slotlockDashboardRequestScope } from './dashboard-scope.js';
29
42
  export { closeSlotlockPartnerMonth, priceSlotlockPartnerMonth, slotlockClientLabel, slotlockPartnerMonthChecksum, } from './partner-billing.js';
30
43
  export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
31
44
  export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emitITipCalendar, expandCalendarEventOccurrences, normalizeCalendarEventContent, normalizeExternalCalendarChange, parseICalendarChanges, parseICalendarFeed, parseTrustedICalendarEvents, recurrenceRuleProblem, storedCalendarEventProblem, } from './sync.js';
32
- export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
33
- export { assertSlotlockSchemaCurrent, calendarEventRollingHorizon, createSlotlockStore, readSlotlockSchemaVersion, SLOTLOCK_AGENT_OWNER_COMMAND_QUOTA, SLOTLOCK_AGENT_OWNER_EVENT_QUOTA, SLOTLOCK_CALENDAR_HORIZON_DAYS, SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS, SLOTLOCK_INTERNAL_EVENT_OWNER_REF, SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE, SLOTLOCK_MATERIALIZATION_WINDOW_DAYS, SLOTLOCK_MAX_EVENT_DURATION_DAYS, SLOTLOCK_TENANT_WRITE_METHODS, } from './store.js';
45
+ export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_APPEND_ONLY_TABLES, SLOTLOCK_AUDIT_RETENTION_DAYS, SLOTLOCK_CHANGE_RETENTION_BY_PLAN, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
46
+ export { assertSlotlockSchemaCurrent, calendarEventRollingHorizon, createSlotlockStore, readSlotlockSchemaVersion, SLOTLOCK_AGENT_OWNER_COMMAND_QUOTA, SLOTLOCK_AGENT_OWNER_EVENT_QUOTA, SLOTLOCK_CALENDAR_HORIZON_DAYS, SLOTLOCK_SOURCE_BUSY_MAX, SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS, SLOTLOCK_INTERNAL_EVENT_OWNER_REF, SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE, SLOTLOCK_MATERIALIZATION_WINDOW_DAYS, SLOTLOCK_MAX_EVENT_DURATION_DAYS, SLOTLOCK_MAX_CONTACTS_PER_TENANT, SLOTLOCK_PAYMENT_HOLD_MAX_TTL_MS, SLOTLOCK_TENANT_WRITE_METHODS, SLOTLOCK_TENANT_EXPORT_HOURS, SLOTLOCK_TENANT_EXPORT_MAX_BYTES, SLOTLOCK_TENANT_EXPORT_PART_ROWS, SLOTLOCK_MAX_TENANT_EXPORTS, buildSlotlockTenantExport, slotlockAuditEventRecord, } from './store.js';
47
+ export { SLOTLOCK_SUBJECT_TABLES, SLOTLOCK_TENANT_EXPORT_LEFT_OUT, SLOTLOCK_TENANT_EXPORT_TABLES, } from './types.js';
package/interop.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type SlotlockExceptionDay } from './hours.js';
1
2
  import type { SlotlockCalendarEvent, WeeklyAvailabilityRule } from './types.js';
2
3
  /** The longest VAVAILABILITY text `parseVAvailability` reads. */
3
4
  export declare const SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS = 65536;
@@ -8,26 +9,43 @@ export interface EmitVAvailabilityOptions {
8
9
  timezone: string;
9
10
  /** The VAVAILABILITY's UID; each AVAILABLE's is this with its index. */
10
11
  uid: string;
12
+ /**
13
+ * Days exceptions replace (RFC 0004 B1): each weekly AVAILABLE gets an EXDATE for its window on
14
+ * them, and the hours an open day has instead are AVAILABLEs of their own, one per window with an
15
+ * RDATE for each further day with the same window.
16
+ */
17
+ days?: readonly SlotlockExceptionDay[];
11
18
  }
19
+ /** The most days one VAVAILABILITY's EXDATEs, RDATEs and single AVAILABLEs may name. */
20
+ export declare const SLOTLOCK_MAX_VAVAILABILITY_DAYS = 1000;
12
21
  /**
13
22
  * A VCALENDAR with one VAVAILABILITY holding an AVAILABLE per rule: DTSTART on the rule's first day
14
23
  * in a reference week of January 2024, with the IANA zone as TZID (no VTIMEZONE: RFC 7809 lets the
15
24
  * two sides agree on IANA names), DURATION and the rule's RRULE. `[]` is a VAVAILABILITY with no
16
25
  * AVAILABLE: never bookable. CRLF line endings, folded at 75 octets.
17
26
  */
18
- export declare function emitVAvailability({ rules, timezone, uid }: EmitVAvailabilityOptions): string;
27
+ export declare function emitVAvailability({ rules, timezone, uid, days }: EmitVAvailabilityOptions): string;
19
28
  export interface ParsedVAvailability {
20
29
  /** The TZID every AVAILABLE named, `UTC` for UTC times, or null when they are all floating. */
21
30
  timezone: string | null;
22
31
  rules: WeeklyAvailabilityRule[];
32
+ /**
33
+ * The days EXDATEs, RDATEs and single AVAILABLEs change, each with the windows it has: none is
34
+ * closed. Empty when the hours are only weekly.
35
+ */
36
+ days: SlotlockExceptionDay[];
23
37
  }
24
38
  /**
25
- * Weekly rules from an iCalendar VAVAILABILITY (RFC 7953): one VAVAILABILITY, each AVAILABLE a
26
- * weekly RRULE with a time of day and a length. Refuses (`invalid_availability`) what weekly
27
- * rules cannot say: a VAVAILABILITY bounded by DTSTART or DTEND, more than one, an AVAILABLE
28
- * without RRULE, with RDATE, EXDATE or RECURRENCE-ID, a whole-day DTSTART, seconds, AVAILABLEs in
29
- * different zones, and any rule `parseAvailabilityRules` refuses. An RRULE without BYDAY takes
30
- * DTSTART's day, as RFC 5545 reads it.
39
+ * Weekly rules and the days that differ from them, from an iCalendar VAVAILABILITY (RFC 7953): one
40
+ * VAVAILABILITY, each AVAILABLE either a weekly RRULE with a time of day and a length, or a single
41
+ * window. EXDATE takes a weekly window off its day, RDATE adds a window (of the AVAILABLE's length,
42
+ * or a PERIOD's), and a single AVAILABLE adds its own: each day they name becomes a day with hours
43
+ * of its own (`days`), as exceptions keep them (RFC 0004 B1). Refuses (`invalid_availability`) what
44
+ * these cannot say: a VAVAILABILITY bounded by DTSTART or DTEND, more than one, RECURRENCE-ID, a
45
+ * whole-day DTSTART, seconds, AVAILABLEs in different zones, an EXDATE at another time than its
46
+ * window, a day with more than four windows or one over a day, more than
47
+ * SLOTLOCK_MAX_VAVAILABILITY_DAYS such days, and any rule `parseAvailabilityRules` refuses. An
48
+ * RRULE without BYDAY takes DTSTART's day, as RFC 5545 reads it.
31
49
  */
32
50
  export declare function parseVAvailability(text: string): ParsedVAvailability;
33
51
  /** A JSCalendar Event object (RFC 8984 §5.1): the properties Slotlock fills. */
package/interop.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // (RFC 8984). Pure functions: the application API serves them, and an application may call them
4
4
  // directly.
5
5
  import ICAL from 'ical.js';
6
+ import { epochDay } from './hours.js';
6
7
  import { zonedDateTimeFields } from './timezone.js';
7
8
  import { invalidAvailability, parseAvailabilityRules } from './weekly-hours.js';
8
9
  /** The longest VAVAILABILITY text `parseVAvailability` reads. */
@@ -40,13 +41,21 @@ function referenceDay(rrule) {
40
41
  const index = DAYS.findIndex((day) => named.includes(day));
41
42
  return new Date(REFERENCE_SUNDAY + Math.max(index, 0) * 86_400_000);
42
43
  }
44
+ /** The most days one VAVAILABILITY's EXDATEs, RDATEs and single AVAILABLEs may name. */
45
+ export const SLOTLOCK_MAX_VAVAILABILITY_DAYS = 1_000;
46
+ const compactDate = (date) => date.replaceAll('-', '');
47
+ const wallStamp = (date, minutes) => `${compactDate(date)}T${pad(Math.floor(minutes / 60))}${pad(minutes % 60)}00`;
48
+ /** The weekday codes an RRULE's BYDAY names. */
49
+ const byDays = (rrule) => new Set((/(?:^|;)BYDAY=([^;]*)/.exec(rrule)?.[1] ?? '').split(',').map((day) => day.slice(-2)));
50
+ /** The weekday code of `YYYY-MM-DD`. */
51
+ const weekdayOf = (date) => DAYS[new Date(`${date}T00:00:00Z`).getUTCDay()];
43
52
  /**
44
53
  * A VCALENDAR with one VAVAILABILITY holding an AVAILABLE per rule: DTSTART on the rule's first day
45
54
  * in a reference week of January 2024, with the IANA zone as TZID (no VTIMEZONE: RFC 7809 lets the
46
55
  * two sides agree on IANA names), DURATION and the rule's RRULE. `[]` is a VAVAILABILITY with no
47
56
  * AVAILABLE: never bookable. CRLF line endings, folded at 75 octets.
48
57
  */
49
- export function emitVAvailability({ rules, timezone, uid }) {
58
+ export function emitVAvailability({ rules, timezone, uid, days }) {
50
59
  const lines = [
51
60
  'BEGIN:VCALENDAR',
52
61
  'VERSION:2.0',
@@ -59,7 +68,31 @@ export function emitVAvailability({ rules, timezone, uid }) {
59
68
  const day = referenceDay(rule.rrule);
60
69
  const hour = Math.floor(rule.startMinutes / 60);
61
70
  const minute = rule.startMinutes % 60;
62
- lines.push('BEGIN:AVAILABLE', `UID:${uid}-${index}`, `DTSTAMP:${VAVAILABILITY_DTSTAMP}`, `DTSTART;TZID=${timezone}:${day.getUTCFullYear()}${pad(day.getUTCMonth() + 1)}${pad(day.getUTCDate())}T${pad(hour)}${pad(minute)}00`, `DURATION:PT${rule.durationMinutes}M`, `RRULE:${rule.rrule}`, 'END:AVAILABLE');
71
+ lines.push('BEGIN:AVAILABLE', `UID:${uid}-${index}`, `DTSTAMP:${VAVAILABILITY_DTSTAMP}`, `DTSTART;TZID=${timezone}:${day.getUTCFullYear()}${pad(day.getUTCMonth() + 1)}${pad(day.getUTCDate())}T${pad(hour)}${pad(minute)}00`, `DURATION:PT${rule.durationMinutes}M`, `RRULE:${rule.rrule}`);
72
+ const weekdays = byDays(rule.rrule);
73
+ const excluded = (days ?? [])
74
+ .filter((day) => weekdays.has(weekdayOf(day.date)))
75
+ .map((day) => wallStamp(day.date, rule.startMinutes));
76
+ if (excluded.length > 0)
77
+ lines.push(`EXDATE;TZID=${timezone}:${excluded.join(',')}`);
78
+ lines.push('END:AVAILABLE');
79
+ });
80
+ // An open day's hours: one AVAILABLE per window, repeated by RDATE on each further day with it.
81
+ const byWindow = new Map();
82
+ for (const day of days ?? []) {
83
+ for (const window of day.windows) {
84
+ const key = `${window.startMinutes}:${window.durationMinutes}`;
85
+ const entry = byWindow.get(key) ?? { window, dates: [] };
86
+ entry.dates.push(day.date);
87
+ byWindow.set(key, entry);
88
+ }
89
+ }
90
+ [...byWindow.values()].forEach(({ window, dates }, index) => {
91
+ const [first, ...rest] = [...dates].sort();
92
+ lines.push('BEGIN:AVAILABLE', `UID:${uid}-day-${index}`, `DTSTAMP:${VAVAILABILITY_DTSTAMP}`, `DTSTART;TZID=${timezone}:${wallStamp(first, window.startMinutes)}`, `DURATION:PT${window.durationMinutes}M`);
93
+ if (rest.length > 0)
94
+ lines.push(`RDATE;TZID=${timezone}:${rest.map((date) => wallStamp(date, window.startMinutes)).join(',')}`);
95
+ lines.push('END:AVAILABLE');
63
96
  });
64
97
  lines.push('END:VAVAILABILITY', 'END:VCALENDAR');
65
98
  return `${lines.map(fold).join(CRLF)}${CRLF}`;
@@ -72,12 +105,16 @@ function single(component, name) {
72
105
  return properties[0] ?? null;
73
106
  }
74
107
  /**
75
- * Weekly rules from an iCalendar VAVAILABILITY (RFC 7953): one VAVAILABILITY, each AVAILABLE a
76
- * weekly RRULE with a time of day and a length. Refuses (`invalid_availability`) what weekly
77
- * rules cannot say: a VAVAILABILITY bounded by DTSTART or DTEND, more than one, an AVAILABLE
78
- * without RRULE, with RDATE, EXDATE or RECURRENCE-ID, a whole-day DTSTART, seconds, AVAILABLEs in
79
- * different zones, and any rule `parseAvailabilityRules` refuses. An RRULE without BYDAY takes
80
- * DTSTART's day, as RFC 5545 reads it.
108
+ * Weekly rules and the days that differ from them, from an iCalendar VAVAILABILITY (RFC 7953): one
109
+ * VAVAILABILITY, each AVAILABLE either a weekly RRULE with a time of day and a length, or a single
110
+ * window. EXDATE takes a weekly window off its day, RDATE adds a window (of the AVAILABLE's length,
111
+ * or a PERIOD's), and a single AVAILABLE adds its own: each day they name becomes a day with hours
112
+ * of its own (`days`), as exceptions keep them (RFC 0004 B1). Refuses (`invalid_availability`) what
113
+ * these cannot say: a VAVAILABILITY bounded by DTSTART or DTEND, more than one, RECURRENCE-ID, a
114
+ * whole-day DTSTART, seconds, AVAILABLEs in different zones, an EXDATE at another time than its
115
+ * window, a day with more than four windows or one over a day, more than
116
+ * SLOTLOCK_MAX_VAVAILABILITY_DAYS such days, and any rule `parseAvailabilityRules` refuses. An
117
+ * RRULE without BYDAY takes DTSTART's day, as RFC 5545 reads it.
81
118
  */
82
119
  export function parseVAvailability(text) {
83
120
  if (text.length > SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS) {
@@ -99,16 +136,26 @@ export function parseVAvailability(text) {
99
136
  throw invalidAvailability('A VAVAILABILITY bounded by DTSTART or DTEND cannot be weekly hours');
100
137
  }
101
138
  const zones = new Set();
102
- const rules = availability.getAllSubcomponents('available').map((available, index) => {
103
- for (const name of ['rdate', 'exdate', 'recurrence-id']) {
104
- if (available.hasProperty(name)) {
105
- throw invalidAvailability(`AVAILABLE ${index} has ${name.toUpperCase()}: weekly hours cannot say it`);
106
- }
139
+ // What EXDATE, RDATE and single AVAILABLEs say, day by day: weekly windows taken away, and
140
+ // windows added.
141
+ const excluded = new Map();
142
+ const added = new Map();
143
+ const named = new Set();
144
+ const nameDay = (date, index) => {
145
+ named.add(date);
146
+ if (named.size > SLOTLOCK_MAX_VAVAILABILITY_DAYS) {
147
+ throw invalidAvailability(`AVAILABLE ${index}: at most ${SLOTLOCK_MAX_VAVAILABILITY_DAYS} days may differ from the week`);
148
+ }
149
+ };
150
+ const weekly = [];
151
+ availability.getAllSubcomponents('available').forEach((available, index) => {
152
+ if (available.hasProperty('recurrence-id')) {
153
+ throw invalidAvailability(`AVAILABLE ${index} has RECURRENCE-ID: weekly hours cannot say it`);
107
154
  }
108
155
  const dtstart = single(available, 'dtstart');
109
156
  const rrule = single(available, 'rrule');
110
- if (!dtstart || !rrule)
111
- throw invalidAvailability(`AVAILABLE ${index} needs DTSTART and RRULE`);
157
+ if (!dtstart)
158
+ throw invalidAvailability(`AVAILABLE ${index} needs DTSTART`);
112
159
  const start = dtstart.getFirstValue();
113
160
  if (start.isDate)
114
161
  throw invalidAvailability(`AVAILABLE ${index} starts on a date, not a time`);
@@ -139,16 +186,97 @@ export function parseVAvailability(text) {
139
186
  if (!Number.isInteger(durationMinutes) || durationMinutes < 1) {
140
187
  throw invalidAvailability(`AVAILABLE ${index} does not last a whole number of minutes`);
141
188
  }
189
+ const startMinutes = start.hour * 60 + start.minute;
190
+ const dateOf = (time) => `${pad(time.year, 4)}-${pad(time.month)}-${pad(time.day)}`;
191
+ // EXDATE and RDATE are read in DTSTART's zone: another would move them.
192
+ const sameZone = (property, name) => {
193
+ if (property.getParameter('tzid') !== dtstart.getParameter('tzid')) {
194
+ throw invalidAvailability(`AVAILABLE ${index} has ${name} in another zone than DTSTART`);
195
+ }
196
+ };
197
+ const addWindow = (date, minutes, length) => {
198
+ if (length > 1_440) {
199
+ throw invalidAvailability(`AVAILABLE ${index} adds a window longer than a day on ${date}`);
200
+ }
201
+ nameDay(date, index);
202
+ added.set(date, [...(added.get(date) ?? []), { startMinutes: minutes, durationMinutes: length }]);
203
+ };
204
+ for (const property of available.getAllProperties('rdate')) {
205
+ sameZone(property, 'RDATE');
206
+ for (const value of property.getValues()) {
207
+ if (value instanceof ICAL.Period) {
208
+ const from = value.start;
209
+ const to = value.getEnd();
210
+ const length = (wallSeconds(to) - wallSeconds(from)) / 60;
211
+ if (from.isDate || from.second !== 0 || !Number.isInteger(length) || length < 1) {
212
+ throw invalidAvailability(`AVAILABLE ${index} has an RDATE period that is not whole minutes`);
213
+ }
214
+ addWindow(dateOf(from), from.hour * 60 + from.minute, length);
215
+ }
216
+ else {
217
+ const time = value;
218
+ if (time.isDate || time.second !== 0) {
219
+ throw invalidAvailability(`AVAILABLE ${index} has an RDATE that is not a time to the minute`);
220
+ }
221
+ addWindow(dateOf(time), time.hour * 60 + time.minute, durationMinutes);
222
+ }
223
+ }
224
+ }
225
+ if (!rrule) {
226
+ // A single window on its day.
227
+ addWindow(dateOf(start), startMinutes, durationMinutes);
228
+ if (available.hasProperty('exdate')) {
229
+ throw invalidAvailability(`AVAILABLE ${index} has EXDATE but no RRULE`);
230
+ }
231
+ return;
232
+ }
142
233
  let value = String(rrule.getFirstValue());
143
234
  if (!/(?:^|;)BYDAY=/.test(value)) {
144
235
  value = `${value};BYDAY=${DAYS[start.dayOfWeek() - 1]}`;
145
236
  }
146
- return { rrule: value, startMinutes: start.hour * 60 + start.minute, durationMinutes };
237
+ const position = weekly.length;
238
+ weekly.push({ rule: { rrule: value, startMinutes, durationMinutes }, index });
239
+ for (const property of available.getAllProperties('exdate')) {
240
+ sameZone(property, 'EXDATE');
241
+ for (const time of property.getValues()) {
242
+ if (!time.isDate && time.hour * 60 + time.minute !== startMinutes) {
243
+ throw invalidAvailability(`AVAILABLE ${index} has an EXDATE at another time than its window`);
244
+ }
245
+ const date = dateOf(time);
246
+ nameDay(date, index);
247
+ excluded.set(date, (excluded.get(date) ?? new Set()).add(position));
248
+ }
249
+ }
147
250
  });
148
251
  if (zones.size > 1)
149
252
  throw invalidAvailability('The AVAILABLEs are in different time zones');
150
253
  const [timezone = null] = zones;
151
- return { timezone, rules: parseAvailabilityRules(rules) };
254
+ const rules = parseAvailabilityRules(weekly.map(({ rule }) => rule));
255
+ // Each named day as it reads: the weekly windows that fall on it and are not excluded, and those
256
+ // added.
257
+ const days = [...named].sort().map((date) => {
258
+ const weekday = weekdayOf(date);
259
+ const untilOk = (rrule) => {
260
+ const until = /(?:^|;)UNTIL=(\d{8})/.exec(rrule)?.[1];
261
+ return until === undefined || compactDate(date) <= until;
262
+ };
263
+ const windows = [];
264
+ rules.forEach((rule, position) => {
265
+ if (!byDays(rule.rrule).has(weekday) || !untilOk(rule.rrule) || excluded.get(date)?.has(position))
266
+ return;
267
+ if (rule.durationMinutes > 1_440) {
268
+ throw invalidAvailability(`A window longer than a day falls on ${date}, which differs from the week`);
269
+ }
270
+ windows.push({ startMinutes: rule.startMinutes, durationMinutes: rule.durationMinutes });
271
+ });
272
+ windows.push(...(added.get(date) ?? []));
273
+ if (windows.length > 4)
274
+ throw invalidAvailability(`${date} has more than 4 windows`);
275
+ if (epochDay(date) === null)
276
+ throw invalidAvailability(`${date} is not a day`);
277
+ return { date, windows };
278
+ });
279
+ return { timezone, rules, days };
152
280
  }
153
281
  const wallSeconds = (time) => Date.UTC(time.year, time.month - 1, time.day, time.hour, time.minute, time.second) / 1000;
154
282
  /** An instant as a LocalDateTime (RFC 8984 §1.4.4) on the wall clock of `timezone`. */
@@ -0,0 +1,26 @@
1
+ import type { SlotlockInvitationSender } from './dashboard.js';
2
+ /** Where invitations are sent from (SLOTLOCK_INVITATION_EMAIL, _KEY, _FROM). */
3
+ export interface SlotlockInvitationEmailConfig {
4
+ provider: 'resend' | 'postmark';
5
+ /** Resend's API key, or Postmark's server token. */
6
+ key: string;
7
+ /** The sender, as the provider has it verified: `Slotlock <team@example.com>` or an address. */
8
+ from: string;
9
+ }
10
+ type Invitation = Parameters<SlotlockInvitationSender['send']>[0];
11
+ /** A provider's refusal: its name and HTTP status alone (its text can echo the address). */
12
+ export declare class SlotlockInvitationEmailError extends Error {
13
+ name: string;
14
+ }
15
+ /** The message an invitation sends: subject, text and HTML saying the same, with no marketing. */
16
+ export declare function slotlockInvitationEmail(invitation: Invitation, brandName?: string): {
17
+ subject: string;
18
+ text: string;
19
+ html: string;
20
+ };
21
+ /** Sends each invitation once through the provider; throws on any answer but success. */
22
+ export declare function createSlotlockInvitationEmailSender(config: SlotlockInvitationEmailConfig, options?: {
23
+ fetch?: typeof fetch;
24
+ brandName?: string;
25
+ }): SlotlockInvitationSender;
26
+ export {};
@@ -0,0 +1,73 @@
1
+ // Team invitations by email (dashboard.ts SlotlockInvitationSender): a plain transactional message
2
+ // with the link, sent through Resend or Postmark with the deployment's own key. Nothing else is
3
+ // sent, and no address is kept by this module.
4
+ import { createHash } from 'node:crypto';
5
+ /** A provider's refusal: its name and HTTP status alone (its text can echo the address). */
6
+ export class SlotlockInvitationEmailError extends Error {
7
+ name = 'SlotlockInvitationEmailError';
8
+ }
9
+ /** Resend refuses a request without a User-Agent (403), and a Worker's fetch sends none. */
10
+ const USER_AGENT = 'Slotlock (+https://slotlock.pylota.io)';
11
+ const escape = (value) => value.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
12
+ /** The message an invitation sends: subject, text and HTML saying the same, with no marketing. */
13
+ export function slotlockInvitationEmail(invitation, brandName = 'Slotlock') {
14
+ const until = `${invitation.expiresAt.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
15
+ const subject = `${invitation.inviter} invited you to ${invitation.workspace} on ${brandName}`;
16
+ const lines = [
17
+ `${invitation.inviter} invited you to join ${invitation.workspace} on ${brandName} as ${invitation.role}.`,
18
+ `Join: ${invitation.link}`,
19
+ `The link works once, for this address, until ${until}. If you did not expect it, ignore this email: nothing happens unless you use the link.`,
20
+ ];
21
+ const html = `<!doctype html><html><body style="font-family:system-ui,sans-serif;line-height:1.5;color:#111"><p>${escape(lines[0])}</p><p><a href="${escape(invitation.link)}">Join ${escape(invitation.workspace)}</a></p><p style="color:#555;font-size:14px">${escape(lines[2])}</p></body></html>`;
22
+ return { subject, text: `${lines.join('\n\n')}\n`, html };
23
+ }
24
+ /** Sends each invitation once through the provider; throws on any answer but success. */
25
+ export function createSlotlockInvitationEmailSender(config, options = {}) {
26
+ const fetchImpl = options.fetch ?? fetch;
27
+ return {
28
+ async send(invitation) {
29
+ const message = slotlockInvitationEmail(invitation, options.brandName);
30
+ const request = config.provider === 'resend'
31
+ ? {
32
+ url: 'https://api.resend.com/emails',
33
+ headers: {
34
+ authorization: `Bearer ${config.key}`,
35
+ 'content-type': 'application/json',
36
+ 'user-agent': USER_AGENT,
37
+ // One send per link, however often the person inviting submits: keyed by the link,
38
+ // as a digest, so the key names no address.
39
+ 'idempotency-key': createHash('sha256').update(invitation.link, 'utf8').digest('hex'),
40
+ },
41
+ body: { from: config.from, to: [invitation.to], subject: message.subject, text: message.text, html: message.html },
42
+ }
43
+ : {
44
+ url: 'https://api.postmarkapp.com/email',
45
+ headers: {
46
+ 'x-postmark-server-token': config.key,
47
+ 'content-type': 'application/json',
48
+ accept: 'application/json',
49
+ 'user-agent': USER_AGENT,
50
+ },
51
+ body: {
52
+ From: config.from,
53
+ To: invitation.to,
54
+ Subject: message.subject,
55
+ TextBody: message.text,
56
+ HtmlBody: message.html,
57
+ MessageStream: 'outbound',
58
+ },
59
+ };
60
+ const response = await fetchImpl(request.url, {
61
+ method: 'POST',
62
+ headers: request.headers,
63
+ body: JSON.stringify(request.body),
64
+ redirect: 'manual',
65
+ signal: AbortSignal.timeout(10_000),
66
+ });
67
+ if (!response.ok) {
68
+ // The status alone: a provider's error text can echo the address.
69
+ throw new SlotlockInvitationEmailError(`${config.provider === 'resend' ? 'Resend' : 'Postmark'} answered HTTP ${response.status}`);
70
+ }
71
+ },
72
+ };
73
+ }
package/lifecycle.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { SlotlockChangeType } from './event-feed.js';
2
+ export declare const SLOTLOCK_BOOKING_STATUSES: readonly ["held", "confirmed", "checked_in", "completed", "cancelled", "no_show", "released", "expired", "erased"];
3
+ export type SlotlockBookingStatus = (typeof SLOTLOCK_BOOKING_STATUSES)[number];
4
+ /** What moves a hold or booking, and who does it. */
5
+ export declare const SLOTLOCK_BOOKING_ACTIONS: readonly ["confirm", "release", "expire", "reschedule", "check_in", "cancel", "no_show", "complete", "erase"];
6
+ export type SlotlockBookingAction = (typeof SLOTLOCK_BOOKING_ACTIONS)[number];
7
+ /** Where an end leaves a hold or booking: nothing moves it again, but erasure. */
8
+ export declare const SLOTLOCK_BOOKING_ENDS: ReadonlySet<SlotlockBookingStatus>;
9
+ /** Why a tombstoned reference ended, as `reservation_tombstones.end_reason` keeps it. */
10
+ export type SlotlockEndReason = 'cancelled' | 'no_show' | 'released' | 'expired' | 'erased';
11
+ type Move = {
12
+ to: SlotlockBookingStatus;
13
+ change: SlotlockChangeType | readonly SlotlockChangeType[] | null;
14
+ };
15
+ /**
16
+ * Each allowed move: the status it leads to and the feed change it writes (null: none; a list: all
17
+ * of them, in order). Anything not here is refused with `SLOTLOCK_BOOKING_REFUSALS`.
18
+ */
19
+ export declare const SLOTLOCK_BOOKING_TRANSITIONS: Readonly<Record<SlotlockBookingStatus, Readonly<Partial<Record<SlotlockBookingAction, Move>>>>>;
20
+ /** The refusal a move gets from a status it is not allowed from, as the store names it. */
21
+ export declare function slotlockBookingRefusal(from: SlotlockBookingStatus, action: SlotlockBookingAction): string;
22
+ /** What `action` does from `from`: the status it leads to and the change it writes, or its refusal. */
23
+ export declare function slotlockBookingNext(from: SlotlockBookingStatus, action: SlotlockBookingAction): {
24
+ ok: true;
25
+ to: SlotlockBookingStatus;
26
+ changes: readonly SlotlockChangeType[];
27
+ } | {
28
+ ok: false;
29
+ code: string;
30
+ };
31
+ export {};
package/lifecycle.js ADDED
@@ -0,0 +1,115 @@
1
+ export const SLOTLOCK_BOOKING_STATUSES = Object.freeze([
2
+ 'held',
3
+ 'confirmed',
4
+ 'checked_in',
5
+ 'completed',
6
+ 'cancelled',
7
+ 'no_show',
8
+ 'released',
9
+ 'expired',
10
+ 'erased',
11
+ ]);
12
+ /** What moves a hold or booking, and who does it. */
13
+ export const SLOTLOCK_BOOKING_ACTIONS = Object.freeze([
14
+ /** The holder books the held time (confirmHold, slotlock_confirm_slot). */
15
+ 'confirm',
16
+ /** The holder lets the hold go (releaseHold, slotlock_release_slots). */
17
+ 'release',
18
+ /** Its time to live runs out; the next writer in its way reaps it. */
19
+ 'expire',
20
+ /** A new time or resource, at the next revision (rescheduleExternalReservation). */
21
+ 'reschedule',
22
+ /** Claimed in person (checkInReservation), from an hour before the start until the end. */
23
+ 'check_in',
24
+ /** Cancelled at the next revision (cancelExternalReservation). */
25
+ 'cancel',
26
+ /** Not checked in within the resource's checkInMinutes: maintenance releases it. */
27
+ 'no_show',
28
+ /** Its end passes: maintenance says so on the feed. */
29
+ 'complete',
30
+ /** The tenant is erased. */
31
+ 'erase',
32
+ ]);
33
+ /** Where an end leaves a hold or booking: nothing moves it again, but erasure. */
34
+ export const SLOTLOCK_BOOKING_ENDS = new Set([
35
+ 'cancelled',
36
+ 'no_show',
37
+ 'released',
38
+ 'expired',
39
+ 'erased',
40
+ ]);
41
+ /**
42
+ * Each allowed move: the status it leads to and the feed change it writes (null: none; a list: all
43
+ * of them, in order). Anything not here is refused with `SLOTLOCK_BOOKING_REFUSALS`.
44
+ */
45
+ export const SLOTLOCK_BOOKING_TRANSITIONS = Object.freeze({
46
+ held: {
47
+ confirm: { to: 'confirmed', change: 'hold.confirmed' },
48
+ release: { to: 'released', change: 'hold.released' },
49
+ expire: { to: 'expired', change: 'hold.expired' },
50
+ reschedule: { to: 'held', change: 'hold.updated' },
51
+ erase: { to: 'erased', change: null },
52
+ },
53
+ confirmed: {
54
+ reschedule: { to: 'confirmed', change: ['reservation.updated', 'reservation.rescheduled'] },
55
+ check_in: { to: 'checked_in', change: 'reservation.checked_in' },
56
+ cancel: { to: 'cancelled', change: 'reservation.cancelled' },
57
+ no_show: { to: 'no_show', change: 'reservation.no_show' },
58
+ complete: { to: 'completed', change: 'reservation.completed' },
59
+ erase: { to: 'erased', change: null },
60
+ },
61
+ checked_in: {
62
+ reschedule: { to: 'checked_in', change: ['reservation.updated', 'reservation.rescheduled'] },
63
+ // Checking in again answers the first check-in.
64
+ check_in: { to: 'checked_in', change: null },
65
+ cancel: { to: 'cancelled', change: 'reservation.cancelled' },
66
+ complete: { to: 'completed', change: 'reservation.completed' },
67
+ erase: { to: 'erased', change: null },
68
+ },
69
+ completed: {
70
+ // A system of record may correct or void a booking after it ended.
71
+ reschedule: { to: 'completed', change: ['reservation.updated', 'reservation.rescheduled'] },
72
+ cancel: { to: 'cancelled', change: 'reservation.cancelled' },
73
+ erase: { to: 'erased', change: null },
74
+ },
75
+ cancelled: { erase: { to: 'erased', change: null } },
76
+ no_show: { erase: { to: 'erased', change: null } },
77
+ released: {},
78
+ expired: {},
79
+ erased: {},
80
+ });
81
+ /** The refusal a move gets from a status it is not allowed from, as the store names it. */
82
+ export function slotlockBookingRefusal(from, action) {
83
+ const holdAction = action === 'confirm' || action === 'release' || action === 'expire';
84
+ switch (from) {
85
+ case 'held':
86
+ // A hold is not a booking: booking operations do not find it.
87
+ return 'reservation_not_found';
88
+ case 'confirmed':
89
+ case 'checked_in':
90
+ if (holdAction)
91
+ return 'hold_not_found';
92
+ // Only maintenance releases no-shows, and never one checked in.
93
+ return 'reservation_checked_in';
94
+ case 'completed':
95
+ return holdAction ? 'hold_not_found' : 'reservation_ended';
96
+ case 'cancelled':
97
+ case 'no_show':
98
+ // The reference is retired: writing it again is refused, checking it in finds nothing.
99
+ return action === 'check_in' ? 'reservation_not_found' : holdAction ? 'hold_not_found' : 'reservation_cancelled';
100
+ case 'released':
101
+ return action === 'confirm' ? 'hold_not_found' : 'not_found';
102
+ case 'expired':
103
+ return action === 'confirm' ? 'hold_expired' : 'not_found';
104
+ case 'erased':
105
+ return 'not_found';
106
+ }
107
+ }
108
+ /** What `action` does from `from`: the status it leads to and the change it writes, or its refusal. */
109
+ export function slotlockBookingNext(from, action) {
110
+ const move = SLOTLOCK_BOOKING_TRANSITIONS[from][action];
111
+ if (!move)
112
+ return { ok: false, code: slotlockBookingRefusal(from, action) };
113
+ const changes = move.change === null ? [] : typeof move.change === 'string' ? [move.change] : move.change;
114
+ return { ok: true, to: move.to, changes };
115
+ }
package/maintenance.d.ts CHANGED
@@ -9,8 +9,8 @@ export interface SlotlockMaintenanceResult {
9
9
  refused: number;
10
10
  /**
11
11
  * Rows deleted: expired holds and their keyed answers; past retention, event idempotency answers,
12
- * agent event cancellations, stray booking retirements and feed changes; and expired assistant
13
- * conversations. A cancelled booking's tombstone stays.
12
+ * agent event cancellations, stray booking retirements and feed changes; expired assistant
13
+ * conversations; and audit events past retention. A cancelled booking's tombstone stays.
14
14
  */
15
15
  pruned: number;
16
16
  /**
@@ -26,6 +26,18 @@ export interface SlotlockMaintenanceResult {
26
26
  waitlistOffers: number;
27
27
  /** Approvals no one decided in time, their held time freed (`approval.expired`). */
28
28
  approvalsExpired: number;
29
+ /** Bookings whose end has passed in the last week, now said on the feed (`reservation.completed`). */
30
+ bookingsCompleted: number;
31
+ /**
32
+ * Event reminders that came due, each now on the feed once (`reminder.due`) for a sender to send
33
+ * (RFC 0004 P3).
34
+ */
35
+ remindersDue: number;
36
+ /**
37
+ * Holiday calendar subscriptions imported again (RFC 0004 B1), with `holidayFetch`; one that
38
+ * could not be read keeps its closures and is tried again a day later.
39
+ */
40
+ holidayCalendarsImported: number;
29
41
  /** More was left than one pass does: run another. */
30
42
  capped: boolean;
31
43
  /**
@@ -49,6 +61,11 @@ export declare function maintainSlotlockTenant(store: SlotlockStore, tenantRef:
49
61
  * (SLOTLOCK_USAGE=on).
50
62
  */
51
63
  usage?: boolean;
64
+ /**
65
+ * Fetches holiday calendar subscriptions due to be imported again (the guarded fetch under
66
+ * `serve`); without it, none is.
67
+ */
68
+ holidayFetch?: typeof fetch;
52
69
  }): Promise<SlotlockMaintenanceResult>;
53
70
  /**
54
71
  * Run `work` holding the tenant's maintenance lock on a connection of its own, or return
@@ -83,6 +100,13 @@ export interface SlotlockMaintenanceSchedulerOptions {
83
100
  batches?: number;
84
101
  /** Roll each tenant's bookings and holds up into its usage, first (maintainSlotlockTenant `usage`). */
85
102
  usage?: boolean;
103
+ /** Fetches holiday calendar subscriptions due to be imported again (maintainSlotlockTenant). */
104
+ holidayFetch?: typeof fetch;
105
+ /**
106
+ * Team workspaces' verified domains looked up again (workspace-domains.ts runSlotlockDomainChecks):
107
+ * run with each sweep, at most every quarter hour. What it throws goes to onError.
108
+ */
109
+ domainChecks?: () => Promise<unknown>;
86
110
  /** A pass that ran: counts only, never the tenant. Not awaited; what it throws is ignored. */
87
111
  onPass?: (result: SlotlockMaintenanceResult) => void;
88
112
  /**