@remit/calendar-service 0.0.18 → 0.0.19

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 (73) hide show
  1. package/dist/accept.d.ts +71 -0
  2. package/dist/accept.js +159 -0
  3. package/dist/build.d.ts +79 -0
  4. package/dist/build.js +196 -0
  5. package/dist/correct.d.ts +4 -0
  6. package/dist/correct.js +22 -0
  7. package/dist/errors.d.ts +56 -0
  8. package/dist/errors.js +1 -0
  9. package/dist/etag.d.ts +9 -0
  10. package/{src/etag.ts → dist/etag.js} +1 -3
  11. package/dist/expand.d.ts +73 -0
  12. package/dist/expand.js +188 -0
  13. package/dist/feed.d.ts +81 -0
  14. package/dist/feed.js +157 -0
  15. package/dist/fixtures.d.ts +19 -0
  16. package/dist/fixtures.js +37 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.js +15 -0
  19. package/dist/memory-store.d.ts +105 -0
  20. package/dist/memory-store.js +319 -0
  21. package/dist/parse.d.ts +39 -0
  22. package/dist/parse.js +87 -0
  23. package/dist/project.d.ts +30 -0
  24. package/dist/project.js +57 -0
  25. package/dist/put.d.ts +52 -0
  26. package/dist/put.js +78 -0
  27. package/dist/reopen.d.ts +14 -0
  28. package/dist/reopen.js +92 -0
  29. package/dist/scope.d.ts +70 -0
  30. package/dist/scope.js +687 -0
  31. package/dist/subscribe.d.ts +96 -0
  32. package/dist/subscribe.js +298 -0
  33. package/dist/suggest.d.ts +74 -0
  34. package/dist/suggest.js +138 -0
  35. package/dist/time.d.ts +76 -0
  36. package/dist/time.js +164 -0
  37. package/dist/window.d.ts +85 -0
  38. package/dist/window.js +161 -0
  39. package/package.json +40 -35
  40. package/src/accept.test.ts +0 -403
  41. package/src/accept.ts +0 -250
  42. package/src/build.test.ts +0 -113
  43. package/src/build.ts +0 -311
  44. package/src/correct.test.ts +0 -173
  45. package/src/correct.ts +0 -40
  46. package/src/errors.ts +0 -62
  47. package/src/etag.test.ts +0 -54
  48. package/src/expand.test.ts +0 -361
  49. package/src/expand.ts +0 -303
  50. package/src/feed.test.ts +0 -334
  51. package/src/feed.ts +0 -203
  52. package/src/feeds/google-secret-address.ics +0 -99
  53. package/src/fixtures.ts +0 -52
  54. package/src/index.ts +0 -128
  55. package/src/memory-store.ts +0 -485
  56. package/src/parse.test.ts +0 -264
  57. package/src/parse.ts +0 -138
  58. package/src/project.test.ts +0 -247
  59. package/src/project.ts +0 -122
  60. package/src/put.test.ts +0 -318
  61. package/src/put.ts +0 -133
  62. package/src/reopen.test.ts +0 -529
  63. package/src/reopen.ts +0 -199
  64. package/src/scope.test.ts +0 -1421
  65. package/src/scope.ts +0 -1003
  66. package/src/subscribe.test.ts +0 -582
  67. package/src/subscribe.ts +0 -433
  68. package/src/suggest.test.ts +0 -376
  69. package/src/suggest.ts +0 -244
  70. package/src/time.ts +0 -260
  71. package/src/window.test.ts +0 -648
  72. package/src/window.ts +0 -281
  73. package/tsconfig.json +0 -8
package/src/feed.ts DELETED
@@ -1,203 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import type {
3
- CalendarCollectionItem,
4
- CalendarObjectItem,
5
- } from "@remit/data-ports";
6
- import ICAL from "ical.js";
7
- import { CALENDAR_PRODID } from "./build.js";
8
- import { computeEtag } from "./etag.js";
9
- import { serializeCalendar } from "./parse.js";
10
-
11
- /** Random bytes behind a feed token (issue #1067). */
12
- export const CALENDAR_FEED_TOKEN_BYTES = 32;
13
-
14
- /** Where a feed lives, as the one place the shape of that path is written. */
15
- export const CALENDAR_FEED_PATH_PREFIX = "/feeds/calendar/";
16
- export const CALENDAR_FEED_PATH_SUFFIX = ".ics";
17
-
18
- /** base64url over `CALENDAR_FEED_TOKEN_BYTES`, so the length is fixed. */
19
- const TOKEN_LENGTH = Math.ceil((CALENDAR_FEED_TOKEN_BYTES * 4) / 3);
20
- const TOKEN_SHAPE = new RegExp(`^[A-Za-z0-9_-]{${TOKEN_LENGTH}}$`);
21
-
22
- /** A minted feed address: the secret, handed out once, and what is stored. */
23
- export interface CalendarFeedSecret {
24
- token: string;
25
- tokenHash: string;
26
- }
27
-
28
- export const hashCalendarFeedToken = (token: string): string =>
29
- createHash("sha256").update(token, "utf8").digest("hex");
30
-
31
- /**
32
- * A fresh feed token and the hash that will stand in for it.
33
- *
34
- * 32 random bytes rather than a stretched password: nothing about this value is
35
- * user-chosen, so there is no dictionary to search and a slow hash would only
36
- * tax every poll of every subscribed client.
37
- */
38
- export const mintCalendarFeedToken = (): CalendarFeedSecret => {
39
- const token = randomBytes(CALENDAR_FEED_TOKEN_BYTES).toString("base64url");
40
- return { token, tokenHash: hashCalendarFeedToken(token) };
41
- };
42
-
43
- /**
44
- * Whether a path segment can be a feed token at all.
45
- *
46
- * Checked before anything is hashed or read, so a request carrying a megabyte
47
- * of path or a `../` never reaches the store. A refusal here is the same 404 a
48
- * revoked token gets — the caller learns nothing from the difference.
49
- */
50
- export const isCalendarFeedToken = (token: string): boolean =>
51
- TOKEN_SHAPE.test(token);
52
-
53
- /**
54
- * The token a request path carries, or `null` when the path is not a feed
55
- * address. One segment only: a slash inside it would name a different route.
56
- */
57
- export const readCalendarFeedToken = (path: string): string | null => {
58
- if (!path.startsWith(CALENDAR_FEED_PATH_PREFIX)) return null;
59
- if (!path.endsWith(CALENDAR_FEED_PATH_SUFFIX)) return null;
60
- const token = path.slice(
61
- CALENDAR_FEED_PATH_PREFIX.length,
62
- -CALENDAR_FEED_PATH_SUFFIX.length,
63
- );
64
- if (token.includes("/")) return null;
65
- return token;
66
- };
67
-
68
- /**
69
- * The feed path with its token taken out, for a log line or a metric label.
70
- *
71
- * The token is the whole credential, so a path that carries one is not a field
72
- * that can be recorded. Redacted rather than dropped: which route was served is
73
- * what an operator reads a log for, and a blank path loses that too.
74
- */
75
- export const redactCalendarFeedPath = (path: string): string =>
76
- readCalendarFeedToken(path) === null
77
- ? path
78
- : `${CALENDAR_FEED_PATH_PREFIX}<redacted>${CALENDAR_FEED_PATH_SUFFIX}`;
79
-
80
- /**
81
- * A TEXT value as RFC 5545 3.3.11 writes one.
82
- *
83
- * ical.js escapes TEXT only for the properties in its design set, and no `X-`
84
- * property is in it, so a value goes out verbatim: a comma or a semicolon
85
- * reads back as a value list, and a line break as the start of a whole
86
- * component the subscriber never had. TEXT has no encoding for a control
87
- * character, so one is dropped rather than served.
88
- */
89
- const escapeIcalText = (value: string): string =>
90
- value
91
- .replace(/\\/g, "\\\\")
92
- .replace(/;/g, "\\;")
93
- .replace(/,/g, "\\,")
94
- .replace(/\r\n|\r|\n/g, "\\n")
95
- // biome-ignore lint/suspicious/noControlCharactersInRegex: RFC 5545 TEXT excludes CONTROL.
96
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
97
-
98
- /** The bytes a feed serves, and what a conditional request needs to skip them. */
99
- export interface CalendarFeed {
100
- icalData: string;
101
- etag: string;
102
- }
103
-
104
- /**
105
- * One VCALENDAR carrying every event in a collection.
106
- *
107
- * The stored components are copied across as they are: a recurring event
108
- * travels as its master plus its RRULE, never as an expansion, because a
109
- * subscriber renders occurrences itself and expects to keep doing so past any
110
- * horizon this server would have chosen. VTIMEZONE definitions come along for
111
- * the same reason — without them a client reads a TZID it cannot resolve.
112
- *
113
- * `X-WR-CALNAME` is a property of the calendar, not of the response: Apple
114
- * Calendar, Google Calendar and Thunderbird all name a subscription from it.
115
- */
116
- export const buildCalendarFeed = (
117
- collection: CalendarCollectionItem,
118
- objects: readonly CalendarObjectItem[],
119
- ): CalendarFeed => {
120
- const feed = new ICAL.Component("vcalendar");
121
- feed.updatePropertyWithValue("prodid", CALENDAR_PRODID);
122
- feed.updatePropertyWithValue("version", "2.0");
123
- feed.updatePropertyWithValue("calscale", "GREGORIAN");
124
- feed.updatePropertyWithValue(
125
- "x-wr-calname",
126
- escapeIcalText(collection.displayName),
127
- );
128
- if (collection.timezone !== "") {
129
- feed.updatePropertyWithValue(
130
- "x-wr-timezone",
131
- escapeIcalText(collection.timezone),
132
- );
133
- }
134
-
135
- const zonesSeen = new Set<string>();
136
- for (const object of objects) {
137
- const stored = new ICAL.Component(ICAL.parse(object.icalData));
138
- // A copy of the list, not the list: adding a component reparents it,
139
- // which splices it out of the array `getAllSubcomponents` handed back and
140
- // would drop whatever followed it.
141
- for (const child of [...stored.getAllSubcomponents()]) {
142
- if (child.name === "vtimezone") {
143
- const tzid = child.getFirstPropertyValue("tzid");
144
- const key = typeof tzid === "string" ? tzid : "";
145
- if (zonesSeen.has(key)) continue;
146
- zonesSeen.add(key);
147
- }
148
- feed.addSubcomponent(child);
149
- }
150
- }
151
-
152
- // ical.js leaves the last content line unterminated. Every line in an
153
- // iCalendar stream ends with CRLF (RFC 5545 3.1), END:VCALENDAR included.
154
- const serialized = serializeCalendar(feed);
155
- const icalData = serialized.endsWith("\r\n")
156
- ? serialized
157
- : `${serialized}\r\n`;
158
- return { icalData, etag: computeEtag(icalData) };
159
- };
160
-
161
- /**
162
- * Whether an `If-None-Match` covers the tag the feed would serve.
163
- *
164
- * Accepts the weak form and `*`, and reads the header as the list RFC 9110
165
- * 13.1.2 says it is — a subscriber that has polled through a proxy may get its
166
- * own tag back with a `W/` on it, and answering 200 to that resends the whole
167
- * calendar for nothing.
168
- */
169
- export const calendarFeedIsUnchanged = (
170
- ifNoneMatch: string | undefined,
171
- etag: string,
172
- ): boolean => {
173
- if (ifNoneMatch === undefined || ifNoneMatch === "") return false;
174
- if (ifNoneMatch.trim() === "*") return true;
175
- return ifNoneMatch
176
- .split(",")
177
- .map((candidate) =>
178
- candidate.trim().replace(/^W\//, "").replace(/^"|"$/g, ""),
179
- )
180
- .includes(etag);
181
- };
182
-
183
- /**
184
- * Whether the calendar has stood still since the date an `If-Modified-Since`
185
- * carries.
186
- *
187
- * Compared at whole seconds, which is the resolution an HTTP-date has: a
188
- * `Last-Modified` served from a millisecond timestamp is rounded down on the
189
- * way out, and comparing the unrounded value against what comes back declares
190
- * every calendar modified within the second it was last written.
191
- *
192
- * An unreadable date is not a condition (RFC 9110 13.1.3) — the full calendar
193
- * is served rather than a 304 nobody asked for.
194
- */
195
- export const calendarFeedIsUnmodifiedSince = (
196
- ifModifiedSince: string | undefined,
197
- lastModifiedAt: number,
198
- ): boolean => {
199
- if (ifModifiedSince === undefined || ifModifiedSince === "") return false;
200
- const since = Date.parse(ifModifiedSince);
201
- if (Number.isNaN(since)) return false;
202
- return Math.floor(lastModifiedAt / 1000) * 1000 <= since;
203
- };
@@ -1,99 +0,0 @@
1
- BEGIN:VCALENDAR
2
- PRODID:-//Google Inc//Google Calendar 70.9054//EN
3
- VERSION:2.0
4
- CALSCALE:GREGORIAN
5
- METHOD:PUBLISH
6
- X-WR-CALNAME:Harbour rota
7
- X-WR-TIMEZONE:Europe/Amsterdam
8
- BEGIN:VTIMEZONE
9
- TZID:Europe/Amsterdam
10
- X-LIC-LOCATION:Europe/Amsterdam
11
- BEGIN:DAYLIGHT
12
- TZOFFSETFROM:+0100
13
- TZOFFSETTO:+0200
14
- TZNAME:CEST
15
- DTSTART:19700329T020000
16
- RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
17
- END:DAYLIGHT
18
- BEGIN:STANDARD
19
- TZOFFSETFROM:+0200
20
- TZOFFSETTO:+0100
21
- TZNAME:CET
22
- DTSTART:19701025T030000
23
- RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
24
- END:STANDARD
25
- END:VTIMEZONE
26
- BEGIN:VTIMEZONE
27
- TZID:America/New_York
28
- BEGIN:DAYLIGHT
29
- TZOFFSETFROM:-0500
30
- TZOFFSETTO:-0400
31
- TZNAME:EDT
32
- DTSTART:19700308T020000
33
- RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
34
- END:DAYLIGHT
35
- BEGIN:STANDARD
36
- TZOFFSETFROM:-0400
37
- TZOFFSETTO:-0500
38
- TZNAME:EST
39
- DTSTART:19701101T020000
40
- RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
41
- END:STANDARD
42
- END:VTIMEZONE
43
- BEGIN:VEVENT
44
- DTSTART;TZID=Europe/Amsterdam:20261005T090000
45
- DTEND;TZID=Europe/Amsterdam:20261005T093000
46
- RRULE:FREQ=WEEKLY;COUNT=6;BYDAY=MO
47
- EXDATE;TZID=Europe/Amsterdam:20261019T090000
48
- DTSTAMP:20260925T120000Z
49
- UID:pilot-standup@google.com
50
- CREATED:20260901T080000Z
51
- LAST-MODIFIED:20260920T080000Z
52
- SEQUENCE:0
53
- STATUS:CONFIRMED
54
- SUMMARY:Pilot standup
55
- TRANSP:OPAQUE
56
- END:VEVENT
57
- BEGIN:VEVENT
58
- DTSTART;TZID=Europe/Amsterdam:20261012T100000
59
- DTEND;TZID=Europe/Amsterdam:20261012T103000
60
- DTSTAMP:20260925T120000Z
61
- UID:pilot-standup@google.com
62
- RECURRENCE-ID;TZID=Europe/Amsterdam:20261012T090000
63
- SEQUENCE:1
64
- STATUS:CONFIRMED
65
- SUMMARY:Pilot standup (moved)
66
- TRANSP:OPAQUE
67
- END:VEVENT
68
- BEGIN:VEVENT
69
- DTSTART;TZID=Europe/Amsterdam:20261102T090000
70
- DTEND;TZID=Europe/Amsterdam:20261102T093000
71
- DTSTAMP:20260925T120000Z
72
- UID:pilot-standup@google.com
73
- RECURRENCE-ID;TZID=Europe/Amsterdam:20261102T090000
74
- SEQUENCE:1
75
- STATUS:CANCELLED
76
- SUMMARY:Pilot standup
77
- TRANSP:OPAQUE
78
- END:VEVENT
79
- BEGIN:VEVENT
80
- DTSTART;VALUE=DATE:20261007
81
- DTEND;VALUE=DATE:20261009
82
- DTSTAMP:20260925T120000Z
83
- UID:quay-offsite@google.com
84
- SEQUENCE:0
85
- STATUS:CONFIRMED
86
- SUMMARY:Quay offsite
87
- TRANSP:TRANSPARENT
88
- END:VEVENT
89
- BEGIN:VEVENT
90
- DTSTART:20261008T140000Z
91
- DTEND:20261008T150000Z
92
- DTSTAMP:20260925T120000Z
93
- UID:tide-review@google.com
94
- SEQUENCE:0
95
- STATUS:CONFIRMED
96
- SUMMARY:Tide table review
97
- TRANSP:OPAQUE
98
- END:VEVENT
99
- END:VCALENDAR
package/src/fixtures.ts DELETED
@@ -1,52 +0,0 @@
1
- /**
2
- * iCalendar fixtures, assembled from lines so the CRLF the RFC requires is
3
- * explicit rather than a property of this file's own line endings.
4
- */
5
- export const ical = (...lines: string[]): string => `${lines.join("\r\n")}\r\n`;
6
-
7
- /**
8
- * The same resource as a client that ignores RFC 5545 3.1 would send it. Real
9
- * `.ics` files arrive LF-only often enough — anything that has been through a
10
- * text-mode checkout, an editor, or a naive copy — that reading them is part of
11
- * the job, not an edge case.
12
- */
13
- export const asLf = (icalData: string): string =>
14
- icalData.replace(/\r\n/g, "\n");
15
-
16
- /**
17
- * Europe/Amsterdam as a client writes it into a resource. Present so a
18
- * DST-crossing series resolves against the definition the resource itself
19
- * carries, which is the only zone data RFC 5545 promises a server.
20
- */
21
- export const AMSTERDAM_VTIMEZONE = [
22
- "BEGIN:VTIMEZONE",
23
- "TZID:Europe/Amsterdam",
24
- "BEGIN:DAYLIGHT",
25
- "TZOFFSETFROM:+0100",
26
- "TZOFFSETTO:+0200",
27
- "TZNAME:CEST",
28
- "DTSTART:19700329T020000",
29
- "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU",
30
- "END:DAYLIGHT",
31
- "BEGIN:STANDARD",
32
- "TZOFFSETFROM:+0200",
33
- "TZOFFSETTO:+0100",
34
- "TZNAME:CET",
35
- "DTSTART:19701025T030000",
36
- "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU",
37
- "END:STANDARD",
38
- "END:VTIMEZONE",
39
- ];
40
-
41
- export const singleEvent = (...eventLines: string[]): string =>
42
- ical(
43
- "BEGIN:VCALENDAR",
44
- "VERSION:2.0",
45
- "PRODID:-//Remit//Calendar Tests//EN",
46
- "BEGIN:VEVENT",
47
- "UID:fixture@example.com",
48
- "DTSTAMP:20260801T090000Z",
49
- ...eventLines,
50
- "END:VEVENT",
51
- "END:VCALENDAR",
52
- );
package/src/index.ts DELETED
@@ -1,128 +0,0 @@
1
- export {
2
- type AcceptCalendarSuggestionInput,
3
- type AcceptedCalendarSuggestion,
4
- acceptCalendarSuggestion,
5
- buildAcceptedCalendar,
6
- } from "./accept.js";
7
- export {
8
- applyEventFields,
9
- buildEventCalendar,
10
- CALENDAR_PRODID,
11
- type CalendarEventFields,
12
- eventTimeFields,
13
- OFFSET_BEARING,
14
- readEventTime,
15
- readRecurrenceRule,
16
- } from "./build.js";
17
- export { correctCalendarSuggestion } from "./correct.js";
18
- export type {
19
- CalendarResult,
20
- CalendarValidationCode,
21
- CalendarValidationError,
22
- } from "./errors.js";
23
- export { computeEtag } from "./etag.js";
24
- export {
25
- CALENDAR_EXPANSION_HORIZON_DAYS,
26
- CALENDAR_EXPANSION_MAX_OCCURRENCES,
27
- CALENDAR_WINDOW_MAX_STEPS,
28
- type CalendarExpansion,
29
- expandCalendar,
30
- expandCalendarWindow,
31
- } from "./expand.js";
32
- export {
33
- buildCalendarFeed,
34
- CALENDAR_FEED_PATH_PREFIX,
35
- CALENDAR_FEED_PATH_SUFFIX,
36
- CALENDAR_FEED_TOKEN_BYTES,
37
- type CalendarFeed,
38
- type CalendarFeedSecret,
39
- calendarFeedIsUnchanged,
40
- calendarFeedIsUnmodifiedSince,
41
- hashCalendarFeedToken,
42
- isCalendarFeedToken,
43
- mintCalendarFeedToken,
44
- readCalendarFeedToken,
45
- redactCalendarFeedPath,
46
- } from "./feed.js";
47
- export {
48
- type ParsedCalendar,
49
- parseCalendar,
50
- serializeCalendar,
51
- } from "./parse.js";
52
- export {
53
- type CalendarObjectProjection,
54
- projectCalendar,
55
- } from "./project.js";
56
- export {
57
- DEFAULT_CALENDAR_URL_SEGMENT,
58
- deleteCalendarObject,
59
- deleteCalendarObjectWrite,
60
- type PutCalendarObjectInput,
61
- prepareCalendarObject,
62
- provisionDefaultCalendar,
63
- putCalendarObject,
64
- readWrittenObject,
65
- } from "./put.js";
66
- export {
67
- answersOvertakenBy,
68
- type ReopenCalendarSuggestionInput,
69
- reopenCalendarSuggestion,
70
- } from "./reopen.js";
71
- export {
72
- applyScopedDelete,
73
- applyScopedUpdate,
74
- findOccurrence,
75
- followingUidOf,
76
- followingUidPrefix,
77
- type RecurrenceScopeValue,
78
- type ScopedWrite,
79
- type ScopedWriteInput,
80
- } from "./scope.js";
81
- export {
82
- CALENDAR_SUBSCRIPTION_MAX_BYTES,
83
- CALENDAR_SUBSCRIPTION_TIMEOUT_MS,
84
- type CalendarFeedFetch,
85
- type CalendarFeedFetcher,
86
- type CalendarFeedResource,
87
- type CalendarFeedSplit,
88
- type CalendarFeedWrite,
89
- type CalendarSubscriptionRefresh,
90
- type CalendarSubscriptionTarget,
91
- fetchCalendarFeed,
92
- isSubscriptionDue,
93
- readCalendarFeed,
94
- readSubscriptionUrl,
95
- recordCalendarFeedFailure,
96
- refreshCalendarSubscription,
97
- splitCalendarFeed,
98
- writeCalendarFeed,
99
- } from "./subscribe.js";
100
- export {
101
- type CalendarSuggestionProjection,
102
- inviteMethodOf,
103
- mailAddressOf,
104
- projectSuggestion,
105
- type RecordCalendarSuggestionInput,
106
- type RecordedCalendarSuggestion,
107
- recordCalendarSuggestion,
108
- } from "./suggest.js";
109
- export {
110
- civilInZone,
111
- isResolvableZone,
112
- type ResolvedTime,
113
- resolveTime,
114
- toOffsetIso,
115
- toUtcIso,
116
- } from "./time.js";
117
- export {
118
- type BusySpan,
119
- CALENDAR_WINDOW_LOOKBACK_DAYS,
120
- type CalendarInstance,
121
- type CalendarWindow,
122
- type CalendarWindowRepositories,
123
- isBusy,
124
- listBusySpans,
125
- listCalendarInstances,
126
- listObjectInstances,
127
- mergeBusySpans,
128
- } from "./window.js";