@remit/backend 0.0.143 → 0.0.145

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 (279) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +5 -0
  4. package/dist/src/api.js +73 -0
  5. package/dist/src/auth.d.ts +24 -0
  6. package/dist/src/auth.js +81 -0
  7. package/dist/src/config/api-config.d.ts +20 -0
  8. package/dist/src/config/api-config.js +1 -0
  9. package/dist/src/config/from-env.d.ts +2 -0
  10. package/dist/src/config/from-env.js +65 -0
  11. package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
  12. package/dist/src/config/msoauth-secrets-manager.js +28 -0
  13. package/dist/src/config/msoauth.d.ts +19 -0
  14. package/dist/src/config/msoauth.js +6 -0
  15. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  16. package/dist/src/derive/autoMoved.js +49 -0
  17. package/dist/src/derive/contentSignature.d.ts +40 -0
  18. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
  19. package/dist/src/derive/contentUrl.d.ts +33 -0
  20. package/dist/src/derive/contentUrl.js +27 -0
  21. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  22. package/dist/src/derive/deriveMuted.js +7 -0
  23. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  24. package/dist/src/derive/enrichThreadRows.js +151 -0
  25. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  26. package/dist/src/derive/filterThreadCriteria.js +24 -0
  27. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  28. package/dist/src/derive/pendingMoveCounts.js +53 -0
  29. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  30. package/dist/src/derive/senderTrust.js +19 -0
  31. package/dist/src/error.d.ts +21 -0
  32. package/dist/src/error.js +85 -0
  33. package/dist/src/handlers/account-guards.d.ts +36 -0
  34. package/dist/src/handlers/account-guards.js +107 -0
  35. package/dist/src/handlers/account-oauth.d.ts +15 -0
  36. package/dist/src/handlers/account-oauth.js +334 -0
  37. package/dist/src/handlers/account-overrides.d.ts +53 -0
  38. package/dist/src/handlers/account-overrides.js +233 -0
  39. package/dist/src/handlers/account-signature.d.ts +28 -0
  40. package/dist/src/handlers/account-signature.js +85 -0
  41. package/dist/src/handlers/account.d.ts +7 -0
  42. package/dist/src/handlers/account.js +337 -0
  43. package/dist/src/handlers/address.d.ts +35 -0
  44. package/dist/src/handlers/address.js +133 -0
  45. package/dist/src/handlers/calendar-event.d.ts +70 -0
  46. package/dist/src/handlers/calendar-event.js +417 -0
  47. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  48. package/dist/src/handlers/calendar-feed.js +184 -0
  49. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  50. package/dist/src/handlers/calendar-suggestion.js +204 -0
  51. package/dist/src/handlers/calendar.d.ts +116 -0
  52. package/dist/src/handlers/calendar.js +323 -0
  53. package/dist/src/handlers/config.d.ts +43 -0
  54. package/dist/src/handlers/config.js +164 -0
  55. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  56. package/dist/src/handlers/ensure-account-config.js +27 -0
  57. package/dist/src/handlers/filter.d.ts +3 -0
  58. package/dist/src/handlers/filter.js +88 -0
  59. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  60. package/dist/src/handlers/folder-role-appointments.js +168 -0
  61. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  62. package/dist/src/handlers/folder-role-labels.js +40 -0
  63. package/dist/src/handlers/folder-role.d.ts +9 -0
  64. package/dist/src/handlers/folder-role.js +37 -0
  65. package/dist/src/handlers/index.d.ts +88 -0
  66. package/dist/src/handlers/index.js +62 -0
  67. package/dist/src/handlers/label.d.ts +43 -0
  68. package/dist/src/handlers/label.js +107 -0
  69. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  70. package/dist/src/handlers/mailbox-bindings.js +61 -0
  71. package/dist/src/handlers/mailbox.d.ts +23 -0
  72. package/dist/src/handlers/mailbox.js +211 -0
  73. package/dist/src/handlers/me.d.ts +2 -0
  74. package/dist/src/handlers/me.js +117 -0
  75. package/dist/src/handlers/message.d.ts +82 -0
  76. package/dist/src/handlers/message.js +595 -0
  77. package/dist/src/handlers/organize.d.ts +10 -0
  78. package/dist/src/handlers/organize.js +103 -0
  79. package/dist/src/handlers/outbox-attachment.d.ts +25 -0
  80. package/dist/src/handlers/outbox-attachment.js +77 -0
  81. package/dist/src/handlers/outbox.d.ts +4 -0
  82. package/dist/src/handlers/outbox.js +136 -0
  83. package/dist/src/handlers/sync-progress.d.ts +46 -0
  84. package/dist/src/handlers/sync-progress.js +63 -0
  85. package/dist/src/handlers/sync.d.ts +28 -0
  86. package/dist/src/handlers/sync.js +73 -0
  87. package/dist/src/handlers/system-update.d.ts +2 -0
  88. package/dist/src/handlers/system-update.js +159 -0
  89. package/dist/src/handlers/thread.d.ts +148 -0
  90. package/dist/src/handlers/thread.js +228 -0
  91. package/dist/src/handlers/unified-threads.d.ts +247 -0
  92. package/dist/src/handlers/unified-threads.js +406 -0
  93. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  94. package/dist/src/handlers/vip-suggestions.js +16 -0
  95. package/dist/src/index.d.ts +5 -0
  96. package/dist/src/index.js +53 -0
  97. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  98. package/dist/src/json.js +10 -0
  99. package/dist/src/jwt-auth.d.ts +15 -0
  100. package/dist/src/jwt-auth.js +45 -0
  101. package/dist/src/lambda.d.ts +13 -0
  102. package/dist/src/lambda.js +33 -0
  103. package/dist/src/request.d.ts +9 -0
  104. package/dist/src/request.js +17 -0
  105. package/dist/src/response.d.ts +44 -0
  106. package/dist/src/response.js +80 -0
  107. package/dist/src/serve.d.ts +31 -0
  108. package/dist/src/serve.js +117 -0
  109. package/dist/src/service/account-deletion.d.ts +20 -0
  110. package/dist/src/service/account-deletion.js +33 -0
  111. package/dist/src/service/compose-sqlite.d.ts +3 -0
  112. package/dist/src/service/compose-sqlite.js +13 -0
  113. package/dist/src/service/create-remit-client.d.ts +109 -0
  114. package/dist/src/service/create-remit-client.js +188 -0
  115. package/dist/src/service/data-client.d.ts +12 -0
  116. package/dist/src/service/data-client.js +42 -0
  117. package/dist/src/service/export-identity.d.ts +4 -0
  118. package/dist/src/service/export-identity.js +33 -0
  119. package/dist/src/service/fire-and-forget.d.ts +54 -0
  120. package/dist/src/service/fire-and-forget.js +37 -0
  121. package/dist/src/service/organize.d.ts +181 -0
  122. package/dist/src/service/organize.js +367 -0
  123. package/dist/src/service/queues.d.ts +12 -0
  124. package/dist/src/service/queues.js +1 -0
  125. package/dist/src/service/refusal.d.ts +4 -0
  126. package/dist/src/service/refusal.js +20 -0
  127. package/dist/src/service/sender-category.d.ts +51 -0
  128. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  129. package/dist/src/service/shared-deps.d.ts +6 -0
  130. package/dist/src/service/shared-deps.js +24 -0
  131. package/dist/src/service/trigger-sync.d.ts +57 -0
  132. package/dist/src/service/trigger-sync.js +31 -0
  133. package/dist/src/types.d.ts +72 -0
  134. package/dist/src/types.js +1 -0
  135. package/package.json +125 -98
  136. package/.env.test +0 -4
  137. package/dev-server/content-auth.test.ts +0 -113
  138. package/dev-server/content-auth.ts +0 -54
  139. package/dev-server/content-handler.test.ts +0 -105
  140. package/dev-server/content-handler.ts +0 -79
  141. package/dev-server/content-path.test.ts +0 -37
  142. package/dev-server/content-path.ts +0 -27
  143. package/dev-server/cors.test.ts +0 -48
  144. package/dev-server/cors.ts +0 -25
  145. package/dev-server/keep-alive.test.ts +0 -24
  146. package/dev-server/lambda-helpers.ts +0 -69
  147. package/dev-server/log-output.test.ts +0 -173
  148. package/dev-server/oauth-callback-route.test.ts +0 -66
  149. package/dev-server/relational-health.test.ts +0 -83
  150. package/dev-server/relational-health.ts +0 -39
  151. package/dev-server/server.ts +0 -384
  152. package/dev-server/sync-age.test.ts +0 -91
  153. package/dev-server/sync-age.ts +0 -55
  154. package/dev-server/upload-handler.ts +0 -132
  155. package/dev-server/upload-route.test.ts +0 -196
  156. package/scripts/backfill-classification.ts +0 -91
  157. package/scripts/backfill-list-id.ts +0 -83
  158. package/scripts/config-save.ts +0 -128
  159. package/src/auth.test.ts +0 -12
  160. package/src/auth.ts +0 -92
  161. package/src/config/msoauth.test.ts +0 -48
  162. package/src/config/msoauth.ts +0 -48
  163. package/src/data-backend.test.ts +0 -23
  164. package/src/data-backend.ts +0 -14
  165. package/src/derive/autoMoved.test.ts +0 -104
  166. package/src/derive/contentSignature.test.ts +0 -161
  167. package/src/derive/contentUrl.test.ts +0 -172
  168. package/src/derive/contentUrl.ts +0 -86
  169. package/src/derive/deriveMuted.test.ts +0 -34
  170. package/src/derive/enrichThreadRows.test.ts +0 -360
  171. package/src/derive/enrichThreadRows.ts +0 -234
  172. package/src/derive/filterThreadCriteria.test.ts +0 -126
  173. package/src/derive/filterThreadCriteria.ts +0 -51
  174. package/src/derive/senderTrust.test.ts +0 -67
  175. package/src/error.test.ts +0 -201
  176. package/src/error.ts +0 -123
  177. package/src/handlers/account-guards.test.ts +0 -67
  178. package/src/handlers/account-guards.ts +0 -169
  179. package/src/handlers/account-oauth-callback.test.ts +0 -155
  180. package/src/handlers/account-oauth-consent.test.ts +0 -336
  181. package/src/handlers/account-oauth-start.test.ts +0 -191
  182. package/src/handlers/account-oauth.test.ts +0 -439
  183. package/src/handlers/account-oauth.ts +0 -635
  184. package/src/handlers/account-overrides.test.ts +0 -88
  185. package/src/handlers/account-overrides.ts +0 -323
  186. package/src/handlers/account-ownership.ts +0 -25
  187. package/src/handlers/account-signature.ts +0 -117
  188. package/src/handlers/account-synced-services.test.ts +0 -245
  189. package/src/handlers/account.ts +0 -552
  190. package/src/handlers/address.test.ts +0 -381
  191. package/src/handlers/address.ts +0 -200
  192. package/src/handlers/calendar-event.test.ts +0 -685
  193. package/src/handlers/calendar-event.ts +0 -738
  194. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  195. package/src/handlers/calendar-feed.test.ts +0 -439
  196. package/src/handlers/calendar-feed.ts +0 -291
  197. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  198. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  199. package/src/handlers/calendar-suggestion.ts +0 -425
  200. package/src/handlers/calendar.test.ts +0 -1080
  201. package/src/handlers/calendar.ts +0 -517
  202. package/src/handlers/config.test.ts +0 -245
  203. package/src/handlers/config.ts +0 -332
  204. package/src/handlers/ensure-account-config.ts +0 -30
  205. package/src/handlers/filter.test.ts +0 -644
  206. package/src/handlers/filter.ts +0 -518
  207. package/src/handlers/folder-role-appointments.test.ts +0 -438
  208. package/src/handlers/folder-role-appointments.ts +0 -249
  209. package/src/handlers/folder-role-labels.ts +0 -63
  210. package/src/handlers/folder-role.test.ts +0 -209
  211. package/src/handlers/folder-role.ts +0 -80
  212. package/src/handlers/index.ts +0 -83
  213. package/src/handlers/label.test.ts +0 -112
  214. package/src/handlers/label.ts +0 -210
  215. package/src/handlers/mailbox-bindings.ts +0 -111
  216. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  217. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  218. package/src/handlers/mailbox.test.ts +0 -280
  219. package/src/handlers/mailbox.ts +0 -581
  220. package/src/handlers/me.test.ts +0 -81
  221. package/src/handlers/me.ts +0 -203
  222. package/src/handlers/message-read-backfill.test.ts +0 -103
  223. package/src/handlers/message.test.ts +0 -131
  224. package/src/handlers/message.ts +0 -1131
  225. package/src/handlers/organize.test.ts +0 -277
  226. package/src/handlers/organize.ts +0 -197
  227. package/src/handlers/outbox-attachment.test.ts +0 -765
  228. package/src/handlers/outbox-attachment.ts +0 -126
  229. package/src/handlers/outbox-list.test.ts +0 -179
  230. package/src/handlers/outbox.test.ts +0 -772
  231. package/src/handlers/outbox.ts +0 -258
  232. package/src/handlers/search.test.ts +0 -161
  233. package/src/handlers/search.ts +0 -105
  234. package/src/handlers/sync-progress.test.ts +0 -205
  235. package/src/handlers/sync-progress.ts +0 -99
  236. package/src/handlers/sync.test.ts +0 -168
  237. package/src/handlers/sync.ts +0 -118
  238. package/src/handlers/system-update.test.ts +0 -497
  239. package/src/handlers/system-update.ts +0 -230
  240. package/src/handlers/thread.test.ts +0 -396
  241. package/src/handlers/thread.ts +0 -427
  242. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  243. package/src/handlers/unified-threads.test.ts +0 -785
  244. package/src/handlers/unified-threads.ts +0 -669
  245. package/src/index.test.ts +0 -83
  246. package/src/index.ts +0 -228
  247. package/src/jwt-auth.test.ts +0 -152
  248. package/src/jwt-auth.ts +0 -132
  249. package/src/log-redaction.test.ts +0 -144
  250. package/src/openapi-spec.test.ts +0 -118
  251. package/src/request-context.test.ts +0 -61
  252. package/src/request-context.ts +0 -33
  253. package/src/request-pipeline.test.ts +0 -88
  254. package/src/request.ts +0 -10
  255. package/src/response.test.ts +0 -130
  256. package/src/response.ts +0 -144
  257. package/src/service/compose-sqlite.ts +0 -98
  258. package/src/service/config-import.ts +0 -39
  259. package/src/service/create-remit-client.ts +0 -522
  260. package/src/service/data-client.test.ts +0 -97
  261. package/src/service/data-client.ts +0 -58
  262. package/src/service/export-identity.ts +0 -39
  263. package/src/service/filter.ts +0 -40
  264. package/src/service/fire-and-forget.test.ts +0 -126
  265. package/src/service/fire-and-forget.ts +0 -79
  266. package/src/service/localhost-env-config.test.ts +0 -36
  267. package/src/service/organize-queue.ts +0 -98
  268. package/src/service/organize.test.ts +0 -1451
  269. package/src/service/organize.ts +0 -1025
  270. package/src/service/read-path-filter-config.test.ts +0 -243
  271. package/src/service/semantic-capability.test.ts +0 -78
  272. package/src/service/semantic-capability.ts +0 -95
  273. package/src/service/sender-category.test.ts +0 -235
  274. package/src/service/sqs.ts +0 -4
  275. package/src/service/trigger-sync.test.ts +0 -205
  276. package/src/service/trigger-sync.ts +0 -100
  277. package/src/stdout-capture-fixture.ts +0 -40
  278. package/src/types.ts +0 -271
  279. package/tsconfig.json +0 -7
@@ -1,738 +0,0 @@
1
- import type {
2
- CalendarEventInstance,
3
- CalendarEventResponse,
4
- CalendarFreeBusySpan,
5
- CreateCalendarEventInput,
6
- UpdateCalendarEventInput,
7
- } from "@remit/api-openapi-types";
8
- import {
9
- applyScopedDelete,
10
- applyScopedUpdate,
11
- buildEventCalendar,
12
- type CalendarEventFields,
13
- type CalendarInstance,
14
- type CalendarWindow,
15
- deleteCalendarObject,
16
- followingUidOf,
17
- listBusySpans,
18
- listCalendarInstances,
19
- listObjectInstances,
20
- OFFSET_BEARING,
21
- parseCalendar,
22
- projectCalendar,
23
- putCalendarObject,
24
- type RecurrenceScopeValue,
25
- type ScopedWrite,
26
- toOffsetIso,
27
- toUtcIso,
28
- } from "@remit/calendar-service";
29
- import type {
30
- CalendarCollectionItem,
31
- CalendarObjectItem,
32
- } from "@remit/data-ports";
33
- import { base36uuid } from "@remit/data-ports/id";
34
- import { RecurrenceScope } from "@remit/domain-enums";
35
- import type { APIGatewayProxyEvent } from "aws-lambda";
36
- import { getAccountConfigIdFromEvent } from "../auth.js";
37
- import { getClient } from "../service/data-client.js";
38
- import type {
39
- CalendarEventDetailOperationIds,
40
- CalendarEventOperationIds,
41
- CalendarFreeBusyOperationIds,
42
- OperationHandler,
43
- } from "../types.js";
44
- import {
45
- badRequest,
46
- type CalendarDeps,
47
- type CalendarOutcome,
48
- calendarDepsOf,
49
- findCalendarFor,
50
- findWritableCalendarFor,
51
- listCalendarsFor,
52
- notFound,
53
- preconditionFailed,
54
- refuseCalendar,
55
- } from "./calendar.js";
56
-
57
- /**
58
- * The widest window a single read may ask for. A year covers every view the
59
- * client has — a year grid is the coarsest — and keeps one request from asking
60
- * the server to expand a decade of a daily series.
61
- */
62
- export const CALENDAR_MAX_WINDOW_DAYS = 366;
63
-
64
- const MAX_WINDOW_MS = CALENDAR_MAX_WINDOW_DAYS * 24 * 60 * 60 * 1000;
65
-
66
- /** Mints the id a new resource is named and identified by. */
67
- export interface CalendarEventDeps extends CalendarDeps {
68
- newId: () => string;
69
- now: () => Date;
70
- }
71
-
72
- export const calendarEventDepsOf = (deps: CalendarDeps): CalendarEventDeps => ({
73
- ...deps,
74
- newId: base36uuid,
75
- now: () => new Date(),
76
- });
77
-
78
- /**
79
- * Reads the window a listing asked for.
80
- *
81
- * Both ends must carry an offset, because a window with no offset names no
82
- * span of time — and a client whose `from` was read an hour out gets an
83
- * apparently empty morning rather than an error.
84
- */
85
- export const readWindow = (
86
- from: unknown,
87
- to: unknown,
88
- ): CalendarOutcome<CalendarWindow> => {
89
- if (typeof from !== "string" || typeof to !== "string") {
90
- return refuseCalendar(
91
- "InvalidWindow",
92
- "a listing needs both `from` and `to`",
93
- );
94
- }
95
- const fromMs = Date.parse(from);
96
- const toMs = Date.parse(to);
97
- if (
98
- Number.isNaN(fromMs) ||
99
- Number.isNaN(toMs) ||
100
- !OFFSET_BEARING.test(from) ||
101
- !OFFSET_BEARING.test(to)
102
- ) {
103
- return refuseCalendar(
104
- "InvalidWindow",
105
- "`from` and `to` must be ISO 8601 date-times with a zone offset",
106
- );
107
- }
108
- if (toMs <= fromMs) {
109
- return refuseCalendar("InvalidWindow", "`to` must come after `from`");
110
- }
111
- if (toMs - fromMs > MAX_WINDOW_MS) {
112
- return refuseCalendar(
113
- "InvalidWindow",
114
- `a window may cover at most ${CALENDAR_MAX_WINDOW_DAYS} days`,
115
- );
116
- }
117
- return { ok: true, value: { from: toUtcIso(fromMs), to: toUtcIso(toMs) } };
118
- };
119
-
120
- /** The collections a listing covers, defaulting to every one the caller holds. */
121
- export const collectionsForListing = async (
122
- deps: CalendarDeps,
123
- accountConfigId: string,
124
- calendarIds: string[],
125
- ): Promise<CalendarOutcome<CalendarCollectionItem[]>> => {
126
- const held = await listCalendarsFor(deps, accountConfigId);
127
- if (calendarIds.length === 0) return { ok: true, value: held };
128
-
129
- const selected = held.filter((collection) =>
130
- calendarIds.includes(collection.calendarId),
131
- );
132
- const missing = calendarIds.find(
133
- (calendarId) =>
134
- !held.some((collection) => collection.calendarId === calendarId),
135
- );
136
- if (missing) {
137
- return refuseCalendar("NotFound", `no calendar ${missing} on this account`);
138
- }
139
- return { ok: true, value: selected };
140
- };
141
-
142
- const toInstanceResponse = (
143
- instance: CalendarInstance,
144
- ): CalendarEventInstance => ({
145
- calendarId: instance.calendarId,
146
- calendarObjectId: instance.calendarObjectId,
147
- recurrenceId: instance.recurrenceId,
148
- icalUid: instance.icalUid,
149
- summary: instance.summary,
150
- start: instance.start,
151
- end: instance.end,
152
- allDay: instance.allDay,
153
- status: instance.status,
154
- transparency: instance.transparency,
155
- zoneCertainty: instance.zoneCertainty,
156
- etag: instance.etag,
157
- hasRecurrence: instance.hasRecurrence,
158
- });
159
-
160
- const toEventResponse = (
161
- object: CalendarObjectItem,
162
- ): CalendarEventResponse => ({
163
- calendarObjectId: object.calendarObjectId,
164
- calendarId: object.calendarId,
165
- resourceName: object.resourceName,
166
- icalUid: object.icalUid,
167
- icalData: object.icalData,
168
- etag: object.etag,
169
- sequence: object.sequence,
170
- syncSequence: object.syncSequence,
171
- summary: object.summary,
172
- dtStart: object.dtStart,
173
- dtEnd: object.dtEnd,
174
- allDay: object.allDay,
175
- zoneCertainty: object.zoneCertainty,
176
- status: object.status,
177
- transparency: object.transparency,
178
- hasRecurrence: object.hasRecurrence,
179
- expandedThrough: object.expandedThrough,
180
- createdAt: object.createdAt,
181
- updatedAt: object.updatedAt,
182
- });
183
-
184
- const eventFieldsOf = (
185
- input: CreateCalendarEventInput,
186
- ): CalendarEventFields => ({
187
- summary: input.summary,
188
- description: input.description ?? "",
189
- location: input.location ?? "",
190
- start: input.start,
191
- end: input.end,
192
- allDay: input.allDay ?? false,
193
- timeZone: input.timeZone ?? "",
194
- status: input.status ?? "Confirmed",
195
- transparency: input.transparency ?? "Opaque",
196
- recurrenceRule: input.recurrenceRule ?? "",
197
- });
198
-
199
- /**
200
- * Reduce a PATCH body to the event fields an update may set, preserving
201
- * absence: a body carrying only `summary` yields only `summary`, and the
202
- * resource's times are then left exactly as they were.
203
- */
204
- export const pickEventUpdate = (
205
- body: Partial<UpdateCalendarEventInput>,
206
- ): Partial<CalendarEventFields> => {
207
- const patch: Partial<CalendarEventFields> = {};
208
- const fields = [
209
- "summary",
210
- "description",
211
- "location",
212
- "start",
213
- "end",
214
- "allDay",
215
- "timeZone",
216
- "status",
217
- "transparency",
218
- "recurrenceRule",
219
- ] as const;
220
- for (const field of fields) {
221
- if (!Object.hasOwn(body, field)) continue;
222
- Object.assign(patch, { [field]: body[field] });
223
- }
224
- return patch;
225
- };
226
-
227
- /**
228
- * Whether an `If-Match` header lets the write through.
229
- *
230
- * No header is no precondition, which is what HTTP says and what a first-party
231
- * client that just read the resource wants. `*` matches any existing resource.
232
- * Quotes and a weak-validator prefix are transport spelling and are stripped
233
- * before comparing, because the stored tag carries neither.
234
- */
235
- export const etagMatches = (
236
- ifMatch: string | undefined,
237
- etag: string,
238
- ): boolean => {
239
- if (ifMatch === undefined || ifMatch === "") return true;
240
- return ifMatch
241
- .split(",")
242
- .map((candidate) => candidate.trim().replace(/^W\//, "").replace(/"/g, ""))
243
- .some((candidate) => candidate === "*" || candidate === etag);
244
- };
245
-
246
- /**
247
- * An absent scope means the whole series, which is what a client editing an
248
- * event that does not recur sends. A scope that is present but not one of the
249
- * three is refused rather than read as `All`: the widest, most destructive
250
- * reading is the worst possible answer to a typo.
251
- */
252
- export const readScope = (
253
- value: unknown,
254
- ): CalendarOutcome<RecurrenceScopeValue> => {
255
- if (value === undefined || value === "") {
256
- return { ok: true, value: RecurrenceScope.All };
257
- }
258
- if (
259
- value === RecurrenceScope.This ||
260
- value === RecurrenceScope.Following ||
261
- value === RecurrenceScope.All
262
- ) {
263
- return { ok: true, value };
264
- }
265
- return refuseCalendar(
266
- "InvalidScope",
267
- `scope must be one of This, Following or All, and this request sent "${String(value)}"`,
268
- );
269
- };
270
-
271
- /**
272
- * Writes what a scoped edit resolved to.
273
- *
274
- * A split is two resources and one write set: the truncated original and the
275
- * remainder land together or not at all, because half a split is a series the
276
- * user sees twice or not at all.
277
- */
278
- export const commitScopedWrite = async (
279
- deps: CalendarEventDeps,
280
- accountConfigId: string,
281
- object: CalendarObjectItem,
282
- collectionTimezone: string,
283
- write: ScopedWrite,
284
- ): Promise<CalendarOutcome<CalendarObjectItem | null>> => {
285
- if (write.kind === "Delete") {
286
- await deleteCalendarObject(deps.calendarUnitOfWork, {
287
- accountConfigId,
288
- calendarId: object.calendarId,
289
- calendarObjectId: object.calendarObjectId,
290
- });
291
- return { ok: true, value: null };
292
- }
293
-
294
- // Both resources are read before either is written. A refusal returned from
295
- // inside the write set would commit it — the truncated head alone, with the
296
- // remainder of the series gone — so nothing that can be refused is left
297
- // inside it, and a refusal from the write path there is a broken invariant
298
- // rather than an outcome.
299
- const checked = await Promise.all(
300
- [write.icalData, ...(write.kind === "Split" ? [write.following] : [])].map(
301
- (icalData) => readWritableCalendar(icalData, collectionTimezone),
302
- ),
303
- );
304
- const refused = checked.find((result) => !result.ok);
305
- if (refused && !refused.ok) return refused;
306
-
307
- return deps.calendarUnitOfWork.transaction(async () => {
308
- const head = await putCalendarObject(deps.calendarUnitOfWork, {
309
- accountConfigId,
310
- calendarId: object.calendarId,
311
- resourceName: object.resourceName,
312
- icalData: write.icalData,
313
- });
314
- if (!head.ok) throw refusedAfterValidation(head.error.code);
315
- if (write.kind === "Replace") return { ok: true, value: head.value };
316
-
317
- const following = await putCalendarObject(deps.calendarUnitOfWork, {
318
- accountConfigId,
319
- calendarId: object.calendarId,
320
- resourceName: `${deps.newId()}.ics`,
321
- icalData: write.following,
322
- });
323
- if (!following.ok) throw refusedAfterValidation(following.error.code);
324
- // The remainder is what the patch applied to, and it lives under an id the
325
- // caller has never seen — returning the truncated head instead would leave
326
- // them holding the resource their edit is not in.
327
- return { ok: true, value: following.value };
328
- });
329
- };
330
-
331
- const refusedAfterValidation = (code: string): Error =>
332
- new Error(
333
- `the calendar write path refused text this request had already validated (${code})`,
334
- );
335
-
336
- /** Reads a resource the same way the write path will, without writing it. */
337
- const readWritableCalendar = async (
338
- icalData: string,
339
- collectionTimezone: string,
340
- ): Promise<CalendarOutcome<null>> => {
341
- const parsed = await parseCalendar(icalData);
342
- if (!parsed.ok) return parsed;
343
- const projected = projectCalendar(parsed.value, collectionTimezone);
344
- if (!projected.ok) return projected;
345
- return { ok: true, value: null };
346
- };
347
-
348
- export const createCalendarEventFor = async (
349
- deps: CalendarEventDeps,
350
- accountConfigId: string,
351
- input: CreateCalendarEventInput,
352
- ): Promise<CalendarOutcome<CalendarObjectItem>> => {
353
- const collection = await findWritableCalendarFor(
354
- deps,
355
- accountConfigId,
356
- input.calendarId,
357
- );
358
- if (!collection.ok) return collection;
359
-
360
- const id = deps.newId();
361
- const built = await buildEventCalendar(
362
- eventFieldsOf(input),
363
- `${id}@reader.remit`,
364
- deps.now(),
365
- );
366
- if (!built.ok) return built;
367
-
368
- return putCalendarObject(deps.calendarUnitOfWork, {
369
- accountConfigId,
370
- calendarId: collection.value.calendarId,
371
- resourceName: `${id}.ics`,
372
- icalData: built.value,
373
- });
374
- };
375
-
376
- export interface ScopedRequest {
377
- calendarId: string;
378
- calendarObjectId: string;
379
- scope: RecurrenceScopeValue;
380
- recurrenceId: string;
381
- ifMatch: string | undefined;
382
- }
383
-
384
- interface ResolvedResource {
385
- collection: CalendarCollectionItem;
386
- object: CalendarObjectItem;
387
- }
388
-
389
- const resolveResource = async (
390
- deps: CalendarEventDeps,
391
- accountConfigId: string,
392
- request: Pick<ScopedRequest, "calendarId" | "calendarObjectId">,
393
- intent: "read" | "write",
394
- ): Promise<CalendarOutcome<ResolvedResource>> => {
395
- const collection =
396
- intent === "write"
397
- ? await findWritableCalendarFor(deps, accountConfigId, request.calendarId)
398
- : await findCalendarFor(deps, accountConfigId, request.calendarId);
399
- if (!collection.ok) return collection;
400
-
401
- const object = await deps.calendarObject.find(
402
- request.calendarId,
403
- request.calendarObjectId,
404
- );
405
- if (!object) {
406
- return refuseCalendar(
407
- "NotFound",
408
- `no event ${request.calendarObjectId} in this calendar`,
409
- );
410
- }
411
- return { ok: true, value: { collection: collection.value, object } };
412
- };
413
-
414
- export const updateCalendarEventFor = async (
415
- deps: CalendarEventDeps,
416
- accountConfigId: string,
417
- request: ScopedRequest,
418
- patch: Partial<CalendarEventFields>,
419
- ): Promise<CalendarOutcome<CalendarObjectItem | null>> => {
420
- const resolved = await resolveResource(
421
- deps,
422
- accountConfigId,
423
- request,
424
- "write",
425
- );
426
- if (!resolved.ok) return resolved;
427
- const { collection, object } = resolved.value;
428
-
429
- if (!etagMatches(request.ifMatch, object.etag)) {
430
- return refuseCalendar(
431
- "EtagMismatch",
432
- "this event has been written since you read it — read it again and reapply the change",
433
- );
434
- }
435
-
436
- const parsed = await parseCalendar(object.icalData);
437
- if (!parsed.ok) {
438
- throw new Error(
439
- `stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`,
440
- );
441
- }
442
-
443
- const write = await applyScopedUpdate(
444
- parsed.value,
445
- collection.timezone,
446
- {
447
- scope: request.scope,
448
- recurrenceId: request.recurrenceId,
449
- followingUid: followingUidOf(
450
- object.icalUid,
451
- `${deps.newId()}@reader.remit`,
452
- ),
453
- },
454
- patch,
455
- );
456
- if (!write.ok) return write;
457
- return commitScopedWrite(
458
- deps,
459
- accountConfigId,
460
- object,
461
- collection.timezone,
462
- write.value,
463
- );
464
- };
465
-
466
- export const deleteCalendarEventFor = async (
467
- deps: CalendarEventDeps,
468
- accountConfigId: string,
469
- request: ScopedRequest,
470
- ): Promise<CalendarOutcome<CalendarObjectItem | null>> => {
471
- const resolved = await resolveResource(
472
- deps,
473
- accountConfigId,
474
- request,
475
- "write",
476
- );
477
- if (!resolved.ok) return resolved;
478
- const { collection, object } = resolved.value;
479
-
480
- if (!etagMatches(request.ifMatch, object.etag)) {
481
- return refuseCalendar(
482
- "EtagMismatch",
483
- "this event has been written since you read it — read it again and reapply the change",
484
- );
485
- }
486
-
487
- const parsed = await parseCalendar(object.icalData);
488
- if (!parsed.ok) {
489
- throw new Error(
490
- `stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`,
491
- );
492
- }
493
-
494
- const write = await applyScopedDelete(parsed.value, collection.timezone, {
495
- scope: request.scope,
496
- recurrenceId: request.recurrenceId,
497
- followingUid: "",
498
- });
499
- if (!write.ok) return write;
500
- return commitScopedWrite(
501
- deps,
502
- accountConfigId,
503
- object,
504
- collection.timezone,
505
- write.value,
506
- );
507
- };
508
-
509
- /** The collection holding a resource, found among the ones given. */
510
- const locateResource = async (
511
- deps: CalendarDeps,
512
- collections: CalendarCollectionItem[],
513
- calendarObjectId: string,
514
- ): Promise<ResolvedResource | undefined> => {
515
- for (const collection of collections) {
516
- const object = await deps.calendarObject.find(
517
- collection.calendarId,
518
- calendarObjectId,
519
- );
520
- if (object) return { collection, object };
521
- }
522
- return undefined;
523
- };
524
-
525
- const readCalendarIds = (value: unknown): string[] => {
526
- if (typeof value === "string") return value === "" ? [] : [value];
527
- if (Array.isArray(value)) return value.filter((id) => typeof id === "string");
528
- return [];
529
- };
530
-
531
- const answerRefusal = (error: { code: string; message: string }) =>
532
- error.code === "NotFound"
533
- ? notFound(error.message)
534
- : error.code === "EtagMismatch"
535
- ? preconditionFailed(error.message)
536
- : badRequest(error);
537
-
538
- const scopedRequestOf = (
539
- context: Parameters<OperationHandler>[0],
540
- ): CalendarOutcome<ScopedRequest> => {
541
- const params = context.request.params as { calendarObjectId: string };
542
- const query = context.request.query as {
543
- calendarId?: string;
544
- scope?: string;
545
- recurrenceId?: string;
546
- };
547
- const scope = readScope(query.scope);
548
- if (!scope.ok) return scope;
549
-
550
- const headers = (context.request.headers ?? {}) as Record<string, string>;
551
- const ifMatch = Object.entries(headers).find(
552
- ([name]) => name.toLowerCase() === "if-match",
553
- )?.[1];
554
- return {
555
- ok: true,
556
- value: {
557
- calendarId: query.calendarId ?? "",
558
- calendarObjectId: params.calendarObjectId,
559
- scope: scope.value,
560
- recurrenceId: query.recurrenceId ?? "",
561
- ifMatch,
562
- },
563
- };
564
- };
565
-
566
- export const CalendarEventOperations: Record<
567
- CalendarEventOperationIds,
568
- OperationHandler<CalendarEventOperationIds>
569
- > = {
570
- CalendarEventOperations_listCalendarEvents: async (
571
- context,
572
- ...args: unknown[]
573
- ) => {
574
- const event = args[0] as APIGatewayProxyEvent;
575
- const accountConfigId = getAccountConfigIdFromEvent(event);
576
- const query = context.request.query as {
577
- from?: string;
578
- to?: string;
579
- calendarId?: string | string[];
580
- calendarObjectId?: string;
581
- };
582
- const window = readWindow(query.from, query.to);
583
- if (!window.ok) return badRequest(window.error);
584
-
585
- const deps = calendarDepsOf(await getClient());
586
- const collections = await collectionsForListing(
587
- deps,
588
- accountConfigId,
589
- readCalendarIds(query.calendarId),
590
- );
591
- if (!collections.ok) return answerRefusal(collections.error);
592
-
593
- if (query.calendarObjectId) {
594
- const located = await locateResource(
595
- deps,
596
- collections.value,
597
- query.calendarObjectId,
598
- );
599
- if (!located) return { items: [] };
600
- const own = await listObjectInstances(
601
- deps,
602
- located.collection,
603
- located.object,
604
- window.value,
605
- );
606
- return { items: own.map(toInstanceResponse) };
607
- }
608
-
609
- const instances = await listCalendarInstances(
610
- deps,
611
- collections.value,
612
- window.value,
613
- );
614
- return { items: instances.map(toInstanceResponse) };
615
- },
616
-
617
- CalendarEventOperations_createCalendarEvent: async (
618
- context,
619
- ...args: unknown[]
620
- ) => {
621
- const event = args[0] as APIGatewayProxyEvent;
622
- const accountConfigId = getAccountConfigIdFromEvent(event);
623
- const input = context.request.requestBody as CreateCalendarEventInput;
624
- const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
625
-
626
- const created = await createCalendarEventFor(deps, accountConfigId, input);
627
- if (!created.ok) return answerRefusal(created.error);
628
- return toEventResponse(created.value);
629
- },
630
- };
631
-
632
- export const CalendarEventDetailOperations: Record<
633
- CalendarEventDetailOperationIds,
634
- OperationHandler<CalendarEventDetailOperationIds>
635
- > = {
636
- CalendarEventDetailOperations_getCalendarEvent: async (
637
- context,
638
- ...args: unknown[]
639
- ) => {
640
- const event = args[0] as APIGatewayProxyEvent;
641
- const accountConfigId = getAccountConfigIdFromEvent(event);
642
- const request = scopedRequestOf(context);
643
- if (!request.ok) return badRequest(request.error);
644
- const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
645
-
646
- if (request.value.calendarId === "") {
647
- const located = await locateResource(
648
- deps,
649
- await listCalendarsFor(deps, accountConfigId),
650
- request.value.calendarObjectId,
651
- );
652
- if (!located) {
653
- return notFound(
654
- `no event ${request.value.calendarObjectId} on this account`,
655
- );
656
- }
657
- return toEventResponse(located.object);
658
- }
659
-
660
- const resolved = await resolveResource(
661
- deps,
662
- accountConfigId,
663
- request.value,
664
- "read",
665
- );
666
- if (!resolved.ok) return answerRefusal(resolved.error);
667
- return toEventResponse(resolved.value.object);
668
- },
669
-
670
- CalendarEventDetailOperations_updateCalendarEvent: async (
671
- context,
672
- ...args: unknown[]
673
- ) => {
674
- const event = args[0] as APIGatewayProxyEvent;
675
- const accountConfigId = getAccountConfigIdFromEvent(event);
676
- const body = context.request
677
- .requestBody as Partial<UpdateCalendarEventInput>;
678
- const request = scopedRequestOf(context);
679
- if (!request.ok) return badRequest(request.error);
680
- const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
681
-
682
- const updated = await updateCalendarEventFor(
683
- deps,
684
- accountConfigId,
685
- request.value,
686
- pickEventUpdate(body),
687
- );
688
- if (!updated.ok) return answerRefusal(updated.error);
689
- if (!updated.value) {
690
- throw new Error("a scoped update resolved to a delete");
691
- }
692
- return toEventResponse(updated.value);
693
- },
694
-
695
- CalendarEventDetailOperations_deleteCalendarEvent: async (
696
- context,
697
- ...args: unknown[]
698
- ) => {
699
- const event = args[0] as APIGatewayProxyEvent;
700
- const accountConfigId = getAccountConfigIdFromEvent(event);
701
- const request = scopedRequestOf(context);
702
- if (!request.ok) return badRequest(request.error);
703
- const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
704
-
705
- const removed = await deleteCalendarEventFor(
706
- deps,
707
- accountConfigId,
708
- request.value,
709
- );
710
- if (!removed.ok) return answerRefusal(removed.error);
711
- return { statusCode: 204 };
712
- },
713
- };
714
-
715
- export const CalendarFreeBusyOperations: Record<
716
- CalendarFreeBusyOperationIds,
717
- OperationHandler<CalendarFreeBusyOperationIds>
718
- > = {
719
- CalendarFreeBusyOperations_listCalendarFreeBusy: async (
720
- context,
721
- ...args: unknown[]
722
- ) => {
723
- const event = args[0] as APIGatewayProxyEvent;
724
- const accountConfigId = getAccountConfigIdFromEvent(event);
725
- const query = context.request.query as { from?: string; to?: string };
726
- const window = readWindow(query.from, query.to);
727
- if (!window.ok) return badRequest(window.error);
728
-
729
- const deps = calendarDepsOf(await getClient());
730
- const collections = await listCalendarsFor(deps, accountConfigId);
731
- const spans = await listBusySpans(deps, collections, window.value);
732
- const items: CalendarFreeBusySpan[] = spans.map((span) => ({
733
- start: toOffsetIso(span.startMs, "UTC"),
734
- end: toOffsetIso(span.endMs, "UTC"),
735
- }));
736
- return { items };
737
- },
738
- };