slotlock 0.7.0 → 0.9.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 (57) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/agent-server.d.ts +25 -5
  3. package/agent-server.js +152 -7
  4. package/agent-store-backend.d.ts +6 -1
  5. package/agent-store-backend.js +861 -55
  6. package/application-api.d.ts +19 -1
  7. package/application-api.js +578 -21
  8. package/approvals.d.ts +2 -0
  9. package/approvals.js +12 -5
  10. package/assistant.d.ts +64 -9
  11. package/assistant.js +450 -82
  12. package/availability-store.d.ts +193 -0
  13. package/availability-store.js +558 -0
  14. package/booking-rules.d.ts +92 -5
  15. package/booking-rules.js +243 -15
  16. package/dashboard-assistant-ui.d.ts +21 -0
  17. package/dashboard-assistant-ui.js +539 -0
  18. package/dashboard-markdown.d.ts +2 -0
  19. package/dashboard-markdown.js +157 -0
  20. package/dashboard-settings-ui.d.ts +1 -0
  21. package/dashboard-settings-ui.js +78 -0
  22. package/dashboard.d.ts +84 -11
  23. package/dashboard.js +1654 -384
  24. package/ddl.d.ts +5 -1
  25. package/ddl.js +760 -9
  26. package/event-feed.d.ts +1 -1
  27. package/event-feed.js +18 -0
  28. package/holiday-import.d.ts +38 -0
  29. package/holiday-import.js +57 -0
  30. package/holidays.d.ts +38 -0
  31. package/holidays.js +150 -0
  32. package/hours.d.ts +125 -0
  33. package/hours.js +224 -0
  34. package/index.d.ts +14 -3
  35. package/index.js +10 -2
  36. package/interop.d.ts +25 -7
  37. package/interop.js +145 -17
  38. package/lifecycle.d.ts +31 -0
  39. package/lifecycle.js +115 -0
  40. package/maintenance.d.ts +19 -0
  41. package/maintenance.js +55 -1
  42. package/metrics.js +3 -0
  43. package/openapi.json +16237 -7795
  44. package/package.json +1 -1
  45. package/ranges.d.ts +28 -0
  46. package/ranges.js +135 -0
  47. package/resource-search.d.ts +19 -0
  48. package/resource-search.js +153 -0
  49. package/rest-api.js +7 -0
  50. package/rules.d.ts +2 -0
  51. package/rules.js +2 -0
  52. package/self-host.d.ts +1 -1
  53. package/self-host.js +221 -21
  54. package/slotlock.manifest.json +1 -1
  55. package/store.d.ts +367 -24
  56. package/store.js +1567 -97
  57. package/types.d.ts +198 -0
package/event-feed.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Sql, TransactionSql } from 'postgres';
2
2
  type SlotlockSql = Sql | TransactionSql;
3
3
  /** What a change can be, as the feed and a webhook's `event_types` name it. */
4
- export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "coverage.stale", "waitlist.offered", "waitlist.expired", "approval.requested", "approval.granted", "approval.declined", "approval.expired", "webhook.disabled", "usage.threshold_reached", "resource.created", "resource.updated", "resource.archived", "resource.restored", "resource.deleted", "coverage.recorded", "coverage.deleted", "webhook.created", "webhook.enabled", "webhook.deleted", "key.created", "key.rotated", "key.revoked"];
4
+ export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "hold.paid", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "reservation.rescheduled", "reservation.completed", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "reservation.timezone_shifted", "reservation.timezone_conflict", "reminder.due", "coverage.stale", "coverage.conflict", "waitlist.offered", "waitlist.expired", "waitlist.joined", "waitlist.accepted", "waitlist.declined", "waitlist.lapsed", "waitlist.left", "approval.requested", "approval.granted", "approval.declined", "approval.expired", "webhook.disabled", "usage.threshold_reached", "resource.created", "resource.updated", "resource.archived", "resource.restored", "resource.deleted", "coverage.recorded", "coverage.deleted", "webhook.created", "webhook.enabled", "webhook.deleted", "key.created", "key.rotated", "key.revoked"];
5
5
  export type SlotlockChangeType = (typeof SLOTLOCK_CHANGE_TYPES)[number];
6
6
  /**
7
7
  * Where a change came from (withTenant's `origin`): an agent protocol (`mcp`, `a2a`), the REST
package/event-feed.js CHANGED
@@ -14,19 +14,37 @@ export const SLOTLOCK_CHANGE_TYPES = Object.freeze([
14
14
  'hold.confirmed',
15
15
  'hold.released',
16
16
  'hold.expired',
17
+ // The integrator's payment for a hold that waits for one (RFC 0004 C3).
18
+ 'hold.paid',
17
19
  'reservation.created',
18
20
  'reservation.updated',
19
21
  'reservation.cancelled',
20
22
  'reservation.checked_in',
21
23
  'reservation.no_show',
24
+ // A booking moved (also reservation.updated), and one whose end has passed (RFC 0004 B4).
25
+ 'reservation.rescheduled',
26
+ 'reservation.completed',
22
27
  'event.created',
23
28
  'event.updated',
24
29
  'event.cancelled',
25
30
  'event.timezone_shifted',
26
31
  'event.timezone_conflict',
32
+ // A reservation that carries its wall time, which now falls at another instant (RFC 0004 B9).
33
+ 'reservation.timezone_shifted',
34
+ 'reservation.timezone_conflict',
35
+ // A stored reminder fell due (RFC 0004 P3): the change's id is the send key.
36
+ 'reminder.due',
27
37
  'coverage.stale',
38
+ // A source reported busy time over a booking Slotlock already holds (RFC 0004 C1, C4).
39
+ 'coverage.conflict',
28
40
  'waitlist.offered',
29
41
  'waitlist.expired',
42
+ // A waitlist entry joined, its offer accepted, declined or lapsed, and an entry left (RFC 0004 B6).
43
+ 'waitlist.joined',
44
+ 'waitlist.accepted',
45
+ 'waitlist.declined',
46
+ 'waitlist.lapsed',
47
+ 'waitlist.left',
30
48
  // A write waiting for a person (approval policies): requested, then granted, declined or expired.
31
49
  'approval.requested',
32
50
  'approval.granted',
@@ -0,0 +1,38 @@
1
+ import { type SlotlockHoliday } from './holidays.js';
2
+ /** Where a holiday calendar comes from, and what to call it. */
3
+ export interface SlotlockHolidaySource {
4
+ /** The iCalendar text, pasted or uploaded. */
5
+ icalendar?: string;
6
+ /** An https URL to fetch it from. */
7
+ url?: string;
8
+ /**
9
+ * The calendar's name at its scope (1-200 characters): importing again under it replaces what
10
+ * it said. By default the calendar's own (X-WR-CALNAME), else the URL's host and path, else
11
+ * "Holidays".
12
+ */
13
+ name?: string;
14
+ /** Keep the URL and import it again every 30 days (maintenance). Needs `url`. */
15
+ subscribe?: boolean;
16
+ /** The first and last days to import, `YYYY-MM-DD`; by default today and two years on. */
17
+ from?: string;
18
+ to?: string;
19
+ }
20
+ /** A holiday calendar read and parsed, ready for importHolidayCalendar. */
21
+ export interface SlotlockHolidayCalendarRead {
22
+ name: string;
23
+ holidays: SlotlockHoliday[];
24
+ /** Events that were not all-day, cancelled, or outside the span. */
25
+ skipped: number;
26
+ span: {
27
+ from: string;
28
+ to: string;
29
+ };
30
+ /** The URL to keep, for a subscription. */
31
+ url?: string;
32
+ }
33
+ /**
34
+ * Read a holiday calendar from its text or URL (`fetcher` fetches it; without one a URL is
35
+ * refused `holiday_url_not_served`), parse it over its span, and name it. Throws
36
+ * `invalid_holiday_calendar` and `holiday_calendar_unreachable`.
37
+ */
38
+ export declare function readSlotlockHolidayCalendar(source: SlotlockHolidaySource, fetcher?: typeof fetch): Promise<SlotlockHolidayCalendarRead>;
@@ -0,0 +1,57 @@
1
+ // Reading a holiday calendar to import (RFC 0004 B1), shared by the application API, the dashboard,
2
+ // the CLI and maintenance: pasted or uploaded text, or an https URL fetched before any transaction
3
+ // opens, parsed over a span of days, and named, so importing it again replaces what it said.
4
+ import { dayToDate, epochDay } from './hours.js';
5
+ import { fetchHolidayCalendar, invalidHolidayCalendar, parseHolidayCalendar } from './holidays.js';
6
+ /** Days a span may cover, and the default. */
7
+ const MAX_SPAN_DAYS = 1_096;
8
+ const DEFAULT_SPAN_DAYS = 730;
9
+ /** A URL's host and path, as a calendar's name. */
10
+ function nameOfUrl(url) {
11
+ try {
12
+ const parsed = new URL(url);
13
+ return [...`${parsed.host}${parsed.pathname === '/' ? '' : parsed.pathname}`].slice(0, 200).join('');
14
+ }
15
+ catch {
16
+ return 'Holidays';
17
+ }
18
+ }
19
+ /**
20
+ * Read a holiday calendar from its text or URL (`fetcher` fetches it; without one a URL is
21
+ * refused `holiday_url_not_served`), parse it over its span, and name it. Throws
22
+ * `invalid_holiday_calendar` and `holiday_calendar_unreachable`.
23
+ */
24
+ export async function readSlotlockHolidayCalendar(source, fetcher) {
25
+ const given = [source.icalendar, source.url].filter((value) => value !== undefined && value !== '');
26
+ if (given.length !== 1)
27
+ throw invalidHolidayCalendar('Give the calendar as text or as a URL, one of them');
28
+ if (source.subscribe === true && !source.url)
29
+ throw invalidHolidayCalendar('Only a calendar read from a URL can be subscribed to');
30
+ const today = Math.floor(Date.now() / 86_400_000);
31
+ const from = source.from ?? dayToDate(today);
32
+ const to = source.to ?? dayToDate((epochDay(from) ?? today) + DEFAULT_SPAN_DAYS);
33
+ const first = epochDay(from);
34
+ const last = epochDay(to);
35
+ if (first === null || last === null || last < first || last - first > MAX_SPAN_DAYS) {
36
+ throw invalidHolidayCalendar('from and to must be days at most three years apart');
37
+ }
38
+ let text;
39
+ if (source.url) {
40
+ if (!fetcher) {
41
+ throw Object.assign(new Error('This server does not fetch calendars by URL'), { code: 'holiday_url_not_served' });
42
+ }
43
+ text = await fetchHolidayCalendar(fetcher, source.url);
44
+ }
45
+ else {
46
+ text = source.icalendar;
47
+ }
48
+ const parsed = parseHolidayCalendar(text, { from, to });
49
+ const name = source.name?.trim() || parsed.name || (source.url ? nameOfUrl(source.url) : 'Holidays');
50
+ return {
51
+ name,
52
+ holidays: parsed.holidays,
53
+ skipped: parsed.skipped,
54
+ span: { from, to },
55
+ ...(source.subscribe === true && source.url ? { url: source.url } : {}),
56
+ };
57
+ }
package/holidays.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /** The longest holiday calendar Slotlock reads: a megabyte. */
2
+ export declare const SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES = 1048576;
3
+ /** Closures one import may make. */
4
+ export declare const SLOTLOCK_MAX_HOLIDAY_DAYS = 1000;
5
+ export interface SlotlockHoliday {
6
+ /** The event's UID, with the day for each occurrence of a recurring one: importing again matches it. */
7
+ uid: string;
8
+ startsOn: string;
9
+ endsOn: string;
10
+ label: string;
11
+ }
12
+ export interface ParsedHolidayCalendar {
13
+ holidays: SlotlockHoliday[];
14
+ /** Events that were not all-day, cancelled, or outside the span. */
15
+ skipped: number;
16
+ /** The calendar's own name (X-WR-CALNAME), when it gives one: at most 200 characters. */
17
+ name?: string;
18
+ }
19
+ export declare function invalidHolidayCalendar(message: string): Error & {
20
+ code: 'invalid_holiday_calendar';
21
+ };
22
+ /**
23
+ * The days off an iCalendar text names from `from` to `to` (`YYYY-MM-DD`, inclusive): each all-day
24
+ * VEVENT, from DTSTART to the day before DTEND (one day without DTEND or DURATION), and each
25
+ * occurrence of a recurring one. Refuses (`invalid_holiday_calendar`) text that is not iCalendar,
26
+ * more than SLOTLOCK_MAX_HOLIDAY_DAYS holidays, and one longer than SLOTLOCK_MAX_EXCEPTION_DAYS.
27
+ */
28
+ export declare function parseHolidayCalendar(text: string, span: {
29
+ from: string;
30
+ to: string;
31
+ }): ParsedHolidayCalendar;
32
+ /**
33
+ * The text at a holiday calendar's URL: `https` only, through `fetch` (give it the guarded fetch, so
34
+ * a private address is refused), at most SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES, within ten seconds.
35
+ * Refuses `invalid_holiday_calendar` for anything else, and `holiday_calendar_unreachable` when it
36
+ * cannot be read.
37
+ */
38
+ export declare function fetchHolidayCalendar(fetcher: typeof fetch, url: string): Promise<string>;
package/holidays.js ADDED
@@ -0,0 +1,150 @@
1
+ // Holiday calendars (RFC 0004 B1): an iCalendar file of days off (a public-holiday feed, a
2
+ // company's closures) read as closures. Each all-day VEVENT closes the local days it covers;
3
+ // recurring ones are expanded over a bounded span. Timed events are not days off and are skipped.
4
+ import ICAL from 'ical.js';
5
+ import { SLOTLOCK_MAX_EXCEPTION_DAYS, dayToDate, epochDay } from './hours.js';
6
+ /** The longest holiday calendar Slotlock reads: a megabyte. */
7
+ export const SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES = 1_048_576;
8
+ /** Closures one import may make. */
9
+ export const SLOTLOCK_MAX_HOLIDAY_DAYS = 1_000;
10
+ /** How long Slotlock waits for a holiday calendar URL. */
11
+ const FETCH_TIMEOUT_MS = 10_000;
12
+ const MAX_LABEL = 200;
13
+ export function invalidHolidayCalendar(message) {
14
+ return Object.assign(new Error(message), { code: 'invalid_holiday_calendar' });
15
+ }
16
+ const pad = (value, width = 2) => String(value).padStart(width, '0');
17
+ const dateOf = (time) => `${pad(time.year, 4)}-${pad(time.month)}-${pad(time.day)}`;
18
+ /**
19
+ * The days off an iCalendar text names from `from` to `to` (`YYYY-MM-DD`, inclusive): each all-day
20
+ * VEVENT, from DTSTART to the day before DTEND (one day without DTEND or DURATION), and each
21
+ * occurrence of a recurring one. Refuses (`invalid_holiday_calendar`) text that is not iCalendar,
22
+ * more than SLOTLOCK_MAX_HOLIDAY_DAYS holidays, and one longer than SLOTLOCK_MAX_EXCEPTION_DAYS.
23
+ */
24
+ export function parseHolidayCalendar(text, span) {
25
+ if (Buffer.byteLength(text) > SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES) {
26
+ throw invalidHolidayCalendar('The calendar is larger than a megabyte');
27
+ }
28
+ const first = epochDay(span.from);
29
+ const last = epochDay(span.to);
30
+ if (first === null || last === null || last < first)
31
+ throw invalidHolidayCalendar('The span is not a range of days');
32
+ let root;
33
+ try {
34
+ root = new ICAL.Component(ICAL.parse(text));
35
+ }
36
+ catch {
37
+ throw invalidHolidayCalendar('The text is not iCalendar');
38
+ }
39
+ if (root.name !== 'vcalendar')
40
+ throw invalidHolidayCalendar('The text is not a VCALENDAR');
41
+ const holidays = [];
42
+ let skipped = 0;
43
+ const add = (holiday) => {
44
+ holidays.push(holiday);
45
+ if (holidays.length > SLOTLOCK_MAX_HOLIDAY_DAYS) {
46
+ throw invalidHolidayCalendar(`The calendar names more than ${SLOTLOCK_MAX_HOLIDAY_DAYS} holidays in the span`);
47
+ }
48
+ };
49
+ for (const component of root.getAllSubcomponents('vevent')) {
50
+ // An override of one occurrence is read with its series.
51
+ if (component.hasProperty('recurrence-id'))
52
+ continue;
53
+ const event = new ICAL.Event(component);
54
+ const start = event.startDate;
55
+ const status = String(component.getFirstPropertyValue('status') ?? '').toUpperCase();
56
+ if (!start || !start.isDate || status === 'CANCELLED') {
57
+ skipped++;
58
+ continue;
59
+ }
60
+ const uid = String(component.getFirstPropertyValue('uid') ?? '').slice(0, 300);
61
+ const summary = String(component.getFirstPropertyValue('summary') ?? '').replace(/\s+/g, ' ').trim();
62
+ const label = [...(summary || 'Holiday')].slice(0, MAX_LABEL).join('');
63
+ const end = event.endDate;
64
+ const startDay = epochDay(dateOf(start));
65
+ // DTEND is the day after the last; without one an all-day event is its one day.
66
+ const length = end && end.isDate ? Math.max(1, epochDay(dateOf(end)) - startDay) : 1;
67
+ if (length > SLOTLOCK_MAX_EXCEPTION_DAYS) {
68
+ throw invalidHolidayCalendar(`"${label}" lasts more than ${SLOTLOCK_MAX_EXCEPTION_DAYS} days`);
69
+ }
70
+ const occurrence = (day, key) => {
71
+ const lastDay = day + length - 1;
72
+ if (lastDay < first || day > last) {
73
+ skipped++;
74
+ return;
75
+ }
76
+ add({ uid: key, startsOn: dayToDate(day), endsOn: dayToDate(lastDay), label });
77
+ };
78
+ if (!event.isRecurring()) {
79
+ occurrence(startDay, uid || `${dateOf(start)}:${label}`);
80
+ continue;
81
+ }
82
+ const iterator = event.iterator();
83
+ for (let next = iterator.next(), steps = 0; next && steps < 5_000; next = iterator.next(), steps++) {
84
+ const day = epochDay(dateOf(next));
85
+ if (day > last)
86
+ break;
87
+ if (day + length - 1 < first)
88
+ continue;
89
+ occurrence(day, `${uid || label}:${dateOf(next)}`);
90
+ }
91
+ }
92
+ const given = String(root.getFirstPropertyValue('x-wr-calname') ?? '').replace(/\s+/g, ' ').trim();
93
+ const name = [...given].slice(0, MAX_LABEL).join('');
94
+ return { holidays, skipped, ...(name ? { name } : {}) };
95
+ }
96
+ /**
97
+ * The text at a holiday calendar's URL: `https` only, through `fetch` (give it the guarded fetch, so
98
+ * a private address is refused), at most SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES, within ten seconds.
99
+ * Refuses `invalid_holiday_calendar` for anything else, and `holiday_calendar_unreachable` when it
100
+ * cannot be read.
101
+ */
102
+ export async function fetchHolidayCalendar(fetcher, url) {
103
+ let parsed;
104
+ try {
105
+ parsed = new URL(url);
106
+ }
107
+ catch {
108
+ throw invalidHolidayCalendar('The calendar URL is not a URL');
109
+ }
110
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {
111
+ throw invalidHolidayCalendar('The calendar URL must be https, without credentials');
112
+ }
113
+ const unreachable = (message) => Object.assign(new Error(message), { code: 'holiday_calendar_unreachable' });
114
+ let response;
115
+ try {
116
+ response = await fetcher(parsed.href, {
117
+ headers: { accept: 'text/calendar' },
118
+ redirect: 'manual',
119
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
120
+ });
121
+ }
122
+ catch (error) {
123
+ if (error.code === 'private_address') {
124
+ throw invalidHolidayCalendar('The calendar URL names a private address');
125
+ }
126
+ throw unreachable('The calendar URL could not be read');
127
+ }
128
+ if (!response.ok)
129
+ throw unreachable(`The calendar URL answered ${response.status}`);
130
+ const declared = Number(response.headers.get('content-length') ?? '0');
131
+ if (declared > SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES)
132
+ throw invalidHolidayCalendar('The calendar is larger than a megabyte');
133
+ const reader = response.body?.getReader();
134
+ if (!reader)
135
+ return '';
136
+ const chunks = [];
137
+ let size = 0;
138
+ for (;;) {
139
+ const { done, value } = await reader.read();
140
+ if (done)
141
+ break;
142
+ size += value.byteLength;
143
+ if (size > SLOTLOCK_MAX_HOLIDAY_CALENDAR_BYTES) {
144
+ await reader.cancel();
145
+ throw invalidHolidayCalendar('The calendar is larger than a megabyte');
146
+ }
147
+ chunks.push(value);
148
+ }
149
+ return new TextDecoder().decode(Buffer.concat(chunks));
150
+ }
package/hours.d.ts ADDED
@@ -0,0 +1,125 @@
1
+ import type { Interval, SlotlockInheritedAvailability, WeeklyAvailabilityRule } from './types.js';
2
+ export type { SlotlockInheritedAvailability };
3
+ /** Where a resource's hours come from. */
4
+ export type SlotlockHoursSource = 'resource' | 'location' | 'tenant' | 'server';
5
+ /** One window of an exception's day, in minutes after local midnight. */
6
+ export interface SlotlockExceptionWindow {
7
+ startMinutes: number;
8
+ /** 1 to 1,440; a window may run past midnight into the next day. */
9
+ durationMinutes: number;
10
+ }
11
+ export type SlotlockExceptionOutcome = 'closed' | 'open';
12
+ export type SlotlockExceptionSource = 'manual' | 'ics' | 'vavailability';
13
+ /** Exceptions a tenant may keep, at all its scopes together. */
14
+ export declare const SLOTLOCK_MAX_AVAILABILITY_EXCEPTIONS = 5000;
15
+ /** The longest one exception may last, in days (both ends counted). */
16
+ export declare const SLOTLOCK_MAX_EXCEPTION_DAYS = 367;
17
+ /** Windows one open exception may give each day. */
18
+ export declare const SLOTLOCK_MAX_EXCEPTION_WINDOWS = 4;
19
+ /** A closure or a day of special hours, for the tenant, a location or one resource. */
20
+ export interface SlotlockAvailabilityException {
21
+ id: string;
22
+ tenantRef: string;
23
+ /** The location it applies to (and every resource inside it), or none. */
24
+ location?: string;
25
+ /** The resource it applies to, or none. Neither: the whole tenant. */
26
+ resourceId?: string;
27
+ /** The first local day, `YYYY-MM-DD`. */
28
+ startsOn: string;
29
+ /** The last local day, `YYYY-MM-DD` (inclusive). */
30
+ endsOn: string;
31
+ outcome: SlotlockExceptionOutcome;
32
+ /** For `open`: the hours each of its days has instead of its usual ones. */
33
+ hours?: SlotlockExceptionWindow[];
34
+ label: string;
35
+ source: SlotlockExceptionSource;
36
+ /** For imports: the source's own id, so importing again updates rather than adds. */
37
+ sourceUid?: string;
38
+ /** The holiday calendar it was imported from (listHolidayCalendars), if any. */
39
+ calendarId?: string;
40
+ createdAt: Date;
41
+ }
42
+ /** An exception as the evaluator applies it, ranked from widest scope (0) to the resource's own. */
43
+ export interface SlotlockRankedException {
44
+ startsOn: string;
45
+ endsOn: string;
46
+ outcome: SlotlockExceptionOutcome;
47
+ hours?: readonly SlotlockExceptionWindow[];
48
+ rank: number;
49
+ }
50
+ export declare function invalidException(message: string): Error & {
51
+ code: 'invalid_availability_exception';
52
+ };
53
+ /** Days since 1970-01-01 for a real `YYYY-MM-DD`, or null. */
54
+ export declare function epochDay(date: string): number | null;
55
+ /** `YYYY-MM-DD` for a day since 1970-01-01. */
56
+ export declare function dayToDate(day: number): string;
57
+ /** An exception's windows from untrusted input, or `invalid_availability_exception`. */
58
+ export declare function parseExceptionWindows(value: unknown): SlotlockExceptionWindow[];
59
+ /**
60
+ * An exception's dates and outcome from untrusted input: real dates, the end not before the start,
61
+ * at most SLOTLOCK_MAX_EXCEPTION_DAYS, and hours exactly when it opens.
62
+ */
63
+ export declare function parseExceptionDays(input: {
64
+ startsOn: unknown;
65
+ endsOn?: unknown;
66
+ outcome: unknown;
67
+ hours?: unknown;
68
+ }): {
69
+ startsOn: string;
70
+ endsOn: string;
71
+ outcome: SlotlockExceptionOutcome;
72
+ hours?: SlotlockExceptionWindow[];
73
+ };
74
+ /** Stored hours that do not read as windows read as none: the day stays closed. */
75
+ export declare function storedExceptionWindows(value: unknown): SlotlockExceptionWindow[];
76
+ /**
77
+ * The open windows of a resource within `window`: its weekly `hours`, with `exceptions` (in rank
78
+ * order) replacing the local days they name in `timezone`. Merged and sorted.
79
+ */
80
+ export declare function openWindows(params: {
81
+ hours: readonly WeeklyAvailabilityRule[];
82
+ exceptions?: readonly SlotlockRankedException[];
83
+ timezone: string;
84
+ window: Interval;
85
+ }): Interval[];
86
+ /**
87
+ * The weekly hours that apply to a resource and where they come from: its own, else what it
88
+ * inherits, else the server's `fallback`.
89
+ */
90
+ export declare function applicableHours(resource: {
91
+ availabilityRules?: readonly WeeklyAvailabilityRule[];
92
+ inheritedAvailability?: SlotlockInheritedAvailability;
93
+ }, fallback: readonly WeeklyAvailabilityRule[]): {
94
+ rules: readonly WeeklyAvailabilityRule[];
95
+ source: SlotlockHoursSource;
96
+ location?: string;
97
+ };
98
+ /** Whether `interval` lies wholly inside the open windows. */
99
+ export declare function coveredByOpenWindows(open: readonly Interval[], interval: Interval): boolean;
100
+ /** The first and last UTC day an exception query for `window` must reach (zones span ±14 hours). */
101
+ export declare function exceptionQueryDays(window: Interval): {
102
+ from: string;
103
+ to: string;
104
+ };
105
+ /** One local day as exceptions leave it: no windows is closed. */
106
+ export interface SlotlockExceptionDay {
107
+ /** `YYYY-MM-DD`. */
108
+ date: string;
109
+ windows: SlotlockExceptionWindow[];
110
+ }
111
+ /**
112
+ * The days from `from` to `to` (`YYYY-MM-DD`, inclusive) that exceptions replace, each as the
113
+ * nearest exception leaves it, by date.
114
+ */
115
+ export declare function exceptionDays(exceptions: readonly SlotlockRankedException[], from: string, to: string): SlotlockExceptionDay[];
116
+ /**
117
+ * Consecutive days that read alike as exception ranges: no windows a closure, else hours of their
118
+ * own. The inverse of `exceptionDays` for one scope.
119
+ */
120
+ export declare function exceptionRanges(days: readonly SlotlockExceptionDay[]): {
121
+ startsOn: string;
122
+ endsOn: string;
123
+ outcome: SlotlockExceptionOutcome;
124
+ hours?: SlotlockExceptionWindow[];
125
+ }[];