@remit/backend 0.0.142 → 0.0.144

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 (269) 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 +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -521
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. package/tsconfig.json +0 -7
@@ -0,0 +1,70 @@
1
+ import type { CreateCalendarEventInput, UpdateCalendarEventInput } from "@remit/api-openapi-types";
2
+ import { type CalendarEventFields, type CalendarWindow, type RecurrenceScopeValue, type ScopedWrite } from "@remit/calendar-service";
3
+ import type { CalendarCollectionItem, CalendarObjectItem } from "@remit/data-ports";
4
+ import type { CalendarEventDetailOperationIds, CalendarEventOperationIds, CalendarFreeBusyOperationIds, OperationHandler } from "../types.js";
5
+ import { type CalendarDeps, type CalendarOutcome } from "./calendar.js";
6
+ /**
7
+ * The widest window a single read may ask for. A year covers every view the
8
+ * client has — a year grid is the coarsest — and keeps one request from asking
9
+ * the server to expand a decade of a daily series.
10
+ */
11
+ export declare const CALENDAR_MAX_WINDOW_DAYS = 366;
12
+ /** Mints the id a new resource is named and identified by. */
13
+ export interface CalendarEventDeps extends CalendarDeps {
14
+ newId: () => string;
15
+ now: () => Date;
16
+ }
17
+ export declare const calendarEventDepsOf: (deps: CalendarDeps) => CalendarEventDeps;
18
+ /**
19
+ * Reads the window a listing asked for.
20
+ *
21
+ * Both ends must carry an offset, because a window with no offset names no
22
+ * span of time — and a client whose `from` was read an hour out gets an
23
+ * apparently empty morning rather than an error.
24
+ */
25
+ export declare const readWindow: (from: unknown, to: unknown) => CalendarOutcome<CalendarWindow>;
26
+ /** The collections a listing covers, defaulting to every one the caller holds. */
27
+ export declare const collectionsForListing: (deps: CalendarDeps, accountConfigId: string, calendarIds: string[]) => Promise<CalendarOutcome<CalendarCollectionItem[]>>;
28
+ /**
29
+ * Reduce a PATCH body to the event fields an update may set, preserving
30
+ * absence: a body carrying only `summary` yields only `summary`, and the
31
+ * resource's times are then left exactly as they were.
32
+ */
33
+ export declare const pickEventUpdate: (body: Partial<UpdateCalendarEventInput>) => Partial<CalendarEventFields>;
34
+ /**
35
+ * Whether an `If-Match` header lets the write through.
36
+ *
37
+ * No header is no precondition, which is what HTTP says and what a first-party
38
+ * client that just read the resource wants. `*` matches any existing resource.
39
+ * Quotes and a weak-validator prefix are transport spelling and are stripped
40
+ * before comparing, because the stored tag carries neither.
41
+ */
42
+ export declare const etagMatches: (ifMatch: string | undefined, etag: string) => boolean;
43
+ /**
44
+ * An absent scope means the whole series, which is what a client editing an
45
+ * event that does not recur sends. A scope that is present but not one of the
46
+ * three is refused rather than read as `All`: the widest, most destructive
47
+ * reading is the worst possible answer to a typo.
48
+ */
49
+ export declare const readScope: (value: unknown) => CalendarOutcome<RecurrenceScopeValue>;
50
+ /**
51
+ * Writes what a scoped edit resolved to.
52
+ *
53
+ * A split is two resources and one write set: the truncated original and the
54
+ * remainder land together or not at all, because half a split is a series the
55
+ * user sees twice or not at all.
56
+ */
57
+ export declare const commitScopedWrite: (deps: CalendarEventDeps, accountConfigId: string, object: CalendarObjectItem, collectionTimezone: string, write: ScopedWrite) => Promise<CalendarOutcome<CalendarObjectItem | null>>;
58
+ export declare const createCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, input: CreateCalendarEventInput) => Promise<CalendarOutcome<CalendarObjectItem>>;
59
+ export interface ScopedRequest {
60
+ calendarId: string;
61
+ calendarObjectId: string;
62
+ scope: RecurrenceScopeValue;
63
+ recurrenceId: string;
64
+ ifMatch: string | undefined;
65
+ }
66
+ export declare const updateCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, request: ScopedRequest, patch: Partial<CalendarEventFields>) => Promise<CalendarOutcome<CalendarObjectItem | null>>;
67
+ export declare const deleteCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, request: ScopedRequest) => Promise<CalendarOutcome<CalendarObjectItem | null>>;
68
+ export declare const CalendarEventOperations: Record<CalendarEventOperationIds, OperationHandler<CalendarEventOperationIds>>;
69
+ export declare const CalendarEventDetailOperations: Record<CalendarEventDetailOperationIds, OperationHandler<CalendarEventDetailOperationIds>>;
70
+ export declare const CalendarFreeBusyOperations: Record<CalendarFreeBusyOperationIds, OperationHandler<CalendarFreeBusyOperationIds>>;
@@ -0,0 +1,425 @@
1
+ import { applyScopedDelete, applyScopedUpdate, buildEventCalendar, deleteCalendarObject, followingUidOf, listBusySpans, listCalendarInstances, listObjectInstances, OFFSET_BEARING, parseCalendar, prepareCalendarObject, putCalendarObject, readWrittenObject, toOffsetIso, toUtcIso, } from "@remit/calendar-service";
2
+ import { base36uuid } from "@remit/data-ports/id";
3
+ import { RecurrenceScope } from "@remit/domain-enums";
4
+ import { getAccountConfigIdFromEvent } from "../auth.js";
5
+ import { getClient } from "../service/data-client.js";
6
+ import { badRequest, calendarDepsOf, findCalendarFor, findWritableCalendarFor, listCalendarsFor, notFound, preconditionFailed, refuseCalendar, } from "./calendar.js";
7
+ /**
8
+ * The widest window a single read may ask for. A year covers every view the
9
+ * client has — a year grid is the coarsest — and keeps one request from asking
10
+ * the server to expand a decade of a daily series.
11
+ */
12
+ export const CALENDAR_MAX_WINDOW_DAYS = 366;
13
+ const MAX_WINDOW_MS = CALENDAR_MAX_WINDOW_DAYS * 24 * 60 * 60 * 1000;
14
+ export const calendarEventDepsOf = (deps) => ({
15
+ ...deps,
16
+ newId: base36uuid,
17
+ now: () => new Date(),
18
+ });
19
+ /**
20
+ * Reads the window a listing asked for.
21
+ *
22
+ * Both ends must carry an offset, because a window with no offset names no
23
+ * span of time — and a client whose `from` was read an hour out gets an
24
+ * apparently empty morning rather than an error.
25
+ */
26
+ export const readWindow = (from, to) => {
27
+ if (typeof from !== "string" || typeof to !== "string") {
28
+ return refuseCalendar("InvalidWindow", "a listing needs both `from` and `to`");
29
+ }
30
+ const fromMs = Date.parse(from);
31
+ const toMs = Date.parse(to);
32
+ if (Number.isNaN(fromMs) ||
33
+ Number.isNaN(toMs) ||
34
+ !OFFSET_BEARING.test(from) ||
35
+ !OFFSET_BEARING.test(to)) {
36
+ return refuseCalendar("InvalidWindow", "`from` and `to` must be ISO 8601 date-times with a zone offset");
37
+ }
38
+ if (toMs <= fromMs) {
39
+ return refuseCalendar("InvalidWindow", "`to` must come after `from`");
40
+ }
41
+ if (toMs - fromMs > MAX_WINDOW_MS) {
42
+ return refuseCalendar("InvalidWindow", `a window may cover at most ${CALENDAR_MAX_WINDOW_DAYS} days`);
43
+ }
44
+ return { ok: true, value: { from: toUtcIso(fromMs), to: toUtcIso(toMs) } };
45
+ };
46
+ /** The collections a listing covers, defaulting to every one the caller holds. */
47
+ export const collectionsForListing = async (deps, accountConfigId, calendarIds) => {
48
+ const held = await listCalendarsFor(deps, accountConfigId);
49
+ if (calendarIds.length === 0)
50
+ return { ok: true, value: held };
51
+ const selected = held.filter((collection) => calendarIds.includes(collection.calendarId));
52
+ const missing = calendarIds.find((calendarId) => !held.some((collection) => collection.calendarId === calendarId));
53
+ if (missing) {
54
+ return refuseCalendar("NotFound", `no calendar ${missing} on this account`);
55
+ }
56
+ return { ok: true, value: selected };
57
+ };
58
+ const toInstanceResponse = (instance) => ({
59
+ calendarId: instance.calendarId,
60
+ calendarObjectId: instance.calendarObjectId,
61
+ recurrenceId: instance.recurrenceId,
62
+ icalUid: instance.icalUid,
63
+ summary: instance.summary,
64
+ start: instance.start,
65
+ end: instance.end,
66
+ allDay: instance.allDay,
67
+ status: instance.status,
68
+ transparency: instance.transparency,
69
+ zoneCertainty: instance.zoneCertainty,
70
+ etag: instance.etag,
71
+ hasRecurrence: instance.hasRecurrence,
72
+ });
73
+ const toEventResponse = (object) => ({
74
+ calendarObjectId: object.calendarObjectId,
75
+ calendarId: object.calendarId,
76
+ resourceName: object.resourceName,
77
+ icalUid: object.icalUid,
78
+ icalData: object.icalData,
79
+ etag: object.etag,
80
+ sequence: object.sequence,
81
+ syncSequence: object.syncSequence,
82
+ summary: object.summary,
83
+ dtStart: object.dtStart,
84
+ dtEnd: object.dtEnd,
85
+ allDay: object.allDay,
86
+ zoneCertainty: object.zoneCertainty,
87
+ status: object.status,
88
+ transparency: object.transparency,
89
+ hasRecurrence: object.hasRecurrence,
90
+ expandedThrough: object.expandedThrough,
91
+ createdAt: object.createdAt,
92
+ updatedAt: object.updatedAt,
93
+ });
94
+ const eventFieldsOf = (input) => ({
95
+ summary: input.summary,
96
+ description: input.description ?? "",
97
+ location: input.location ?? "",
98
+ start: input.start,
99
+ end: input.end,
100
+ allDay: input.allDay ?? false,
101
+ timeZone: input.timeZone ?? "",
102
+ status: input.status ?? "Confirmed",
103
+ transparency: input.transparency ?? "Opaque",
104
+ recurrenceRule: input.recurrenceRule ?? "",
105
+ });
106
+ /**
107
+ * Reduce a PATCH body to the event fields an update may set, preserving
108
+ * absence: a body carrying only `summary` yields only `summary`, and the
109
+ * resource's times are then left exactly as they were.
110
+ */
111
+ export const pickEventUpdate = (body) => {
112
+ const patch = {};
113
+ const fields = [
114
+ "summary",
115
+ "description",
116
+ "location",
117
+ "start",
118
+ "end",
119
+ "allDay",
120
+ "timeZone",
121
+ "status",
122
+ "transparency",
123
+ "recurrenceRule",
124
+ ];
125
+ for (const field of fields) {
126
+ if (!Object.hasOwn(body, field))
127
+ continue;
128
+ Object.assign(patch, { [field]: body[field] });
129
+ }
130
+ return patch;
131
+ };
132
+ /**
133
+ * Whether an `If-Match` header lets the write through.
134
+ *
135
+ * No header is no precondition, which is what HTTP says and what a first-party
136
+ * client that just read the resource wants. `*` matches any existing resource.
137
+ * Quotes and a weak-validator prefix are transport spelling and are stripped
138
+ * before comparing, because the stored tag carries neither.
139
+ */
140
+ export const etagMatches = (ifMatch, etag) => {
141
+ if (ifMatch === undefined || ifMatch === "")
142
+ return true;
143
+ return ifMatch
144
+ .split(",")
145
+ .map((candidate) => candidate.trim().replace(/^W\//, "").replace(/"/g, ""))
146
+ .some((candidate) => candidate === "*" || candidate === etag);
147
+ };
148
+ /**
149
+ * An absent scope means the whole series, which is what a client editing an
150
+ * event that does not recur sends. A scope that is present but not one of the
151
+ * three is refused rather than read as `All`: the widest, most destructive
152
+ * reading is the worst possible answer to a typo.
153
+ */
154
+ export const readScope = (value) => {
155
+ if (value === undefined || value === "") {
156
+ return { ok: true, value: RecurrenceScope.All };
157
+ }
158
+ if (value === RecurrenceScope.This ||
159
+ value === RecurrenceScope.Following ||
160
+ value === RecurrenceScope.All) {
161
+ return { ok: true, value };
162
+ }
163
+ return refuseCalendar("InvalidScope", `scope must be one of This, Following or All, and this request sent "${String(value)}"`);
164
+ };
165
+ /**
166
+ * Writes what a scoped edit resolved to.
167
+ *
168
+ * A split is two resources and one write set: the truncated original and the
169
+ * remainder land together or not at all, because half a split is a series the
170
+ * user sees twice or not at all.
171
+ */
172
+ export const commitScopedWrite = async (deps, accountConfigId, object, collectionTimezone, write) => {
173
+ if (write.kind === "Delete") {
174
+ await deleteCalendarObject(deps.calendarStore, {
175
+ accountConfigId,
176
+ calendarId: object.calendarId,
177
+ calendarObjectId: object.calendarObjectId,
178
+ });
179
+ return { ok: true, value: null };
180
+ }
181
+ // Both resources are prepared before either is written, so a refusal of
182
+ // either one commits nothing: half a split is never stored.
183
+ const zone = { timezone: collectionTimezone };
184
+ const head = await prepareCalendarObject(zone, {
185
+ accountConfigId,
186
+ calendarId: object.calendarId,
187
+ resourceName: object.resourceName,
188
+ icalData: write.icalData,
189
+ });
190
+ if (!head.ok)
191
+ return head;
192
+ if (write.kind === "Replace") {
193
+ await deps.calendarStore.commit([head.value]);
194
+ return {
195
+ ok: true,
196
+ value: await readWrittenObject(deps.calendarStore, head.value),
197
+ };
198
+ }
199
+ const following = await prepareCalendarObject(zone, {
200
+ accountConfigId,
201
+ calendarId: object.calendarId,
202
+ resourceName: `${deps.newId()}.ics`,
203
+ icalData: write.following,
204
+ });
205
+ if (!following.ok)
206
+ return following;
207
+ await deps.calendarStore.commit([head.value, following.value]);
208
+ // The remainder is what the patch applied to, and it lives under an id the
209
+ // caller has never seen — returning the truncated head instead would leave
210
+ // them holding the resource their edit is not in.
211
+ return {
212
+ ok: true,
213
+ value: await readWrittenObject(deps.calendarStore, following.value),
214
+ };
215
+ };
216
+ export const createCalendarEventFor = async (deps, accountConfigId, input) => {
217
+ const collection = await findWritableCalendarFor(deps, accountConfigId, input.calendarId);
218
+ if (!collection.ok)
219
+ return collection;
220
+ const id = deps.newId();
221
+ const built = await buildEventCalendar(eventFieldsOf(input), `${id}@reader.remit`, deps.now());
222
+ if (!built.ok)
223
+ return built;
224
+ return putCalendarObject(deps.calendarStore, {
225
+ accountConfigId,
226
+ calendarId: collection.value.calendarId,
227
+ resourceName: `${id}.ics`,
228
+ icalData: built.value,
229
+ });
230
+ };
231
+ const resolveResource = async (deps, accountConfigId, request, intent) => {
232
+ const collection = intent === "write"
233
+ ? await findWritableCalendarFor(deps, accountConfigId, request.calendarId)
234
+ : await findCalendarFor(deps, accountConfigId, request.calendarId);
235
+ if (!collection.ok)
236
+ return collection;
237
+ const object = await deps.calendarObject.find(request.calendarId, request.calendarObjectId);
238
+ if (!object) {
239
+ return refuseCalendar("NotFound", `no event ${request.calendarObjectId} in this calendar`);
240
+ }
241
+ return { ok: true, value: { collection: collection.value, object } };
242
+ };
243
+ export const updateCalendarEventFor = async (deps, accountConfigId, request, patch) => {
244
+ const resolved = await resolveResource(deps, accountConfigId, request, "write");
245
+ if (!resolved.ok)
246
+ return resolved;
247
+ const { collection, object } = resolved.value;
248
+ if (!etagMatches(request.ifMatch, object.etag)) {
249
+ return refuseCalendar("EtagMismatch", "this event has been written since you read it — read it again and reapply the change");
250
+ }
251
+ const parsed = await parseCalendar(object.icalData);
252
+ if (!parsed.ok) {
253
+ throw new Error(`stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`);
254
+ }
255
+ const write = await applyScopedUpdate(parsed.value, collection.timezone, {
256
+ scope: request.scope,
257
+ recurrenceId: request.recurrenceId,
258
+ followingUid: followingUidOf(object.icalUid, `${deps.newId()}@reader.remit`),
259
+ }, patch);
260
+ if (!write.ok)
261
+ return write;
262
+ return commitScopedWrite(deps, accountConfigId, object, collection.timezone, write.value);
263
+ };
264
+ export const deleteCalendarEventFor = async (deps, accountConfigId, request) => {
265
+ const resolved = await resolveResource(deps, accountConfigId, request, "write");
266
+ if (!resolved.ok)
267
+ return resolved;
268
+ const { collection, object } = resolved.value;
269
+ if (!etagMatches(request.ifMatch, object.etag)) {
270
+ return refuseCalendar("EtagMismatch", "this event has been written since you read it — read it again and reapply the change");
271
+ }
272
+ const parsed = await parseCalendar(object.icalData);
273
+ if (!parsed.ok) {
274
+ throw new Error(`stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`);
275
+ }
276
+ const write = await applyScopedDelete(parsed.value, collection.timezone, {
277
+ scope: request.scope,
278
+ recurrenceId: request.recurrenceId,
279
+ followingUid: "",
280
+ });
281
+ if (!write.ok)
282
+ return write;
283
+ return commitScopedWrite(deps, accountConfigId, object, collection.timezone, write.value);
284
+ };
285
+ /** The collection holding a resource, found among the ones given. */
286
+ const locateResource = async (deps, collections, calendarObjectId) => {
287
+ for (const collection of collections) {
288
+ const object = await deps.calendarObject.find(collection.calendarId, calendarObjectId);
289
+ if (object)
290
+ return { collection, object };
291
+ }
292
+ return undefined;
293
+ };
294
+ const readCalendarIds = (value) => {
295
+ if (typeof value === "string")
296
+ return value === "" ? [] : [value];
297
+ if (Array.isArray(value))
298
+ return value.filter((id) => typeof id === "string");
299
+ return [];
300
+ };
301
+ const answerRefusal = (error) => error.code === "NotFound"
302
+ ? notFound(error.message)
303
+ : error.code === "EtagMismatch"
304
+ ? preconditionFailed(error.message)
305
+ : badRequest(error);
306
+ const scopedRequestOf = (context) => {
307
+ const params = context.request.params;
308
+ const query = context.request.query;
309
+ const scope = readScope(query.scope);
310
+ if (!scope.ok)
311
+ return scope;
312
+ const headers = (context.request.headers ?? {});
313
+ const ifMatch = Object.entries(headers).find(([name]) => name.toLowerCase() === "if-match")?.[1];
314
+ return {
315
+ ok: true,
316
+ value: {
317
+ calendarId: query.calendarId ?? "",
318
+ calendarObjectId: params.calendarObjectId,
319
+ scope: scope.value,
320
+ recurrenceId: query.recurrenceId ?? "",
321
+ ifMatch,
322
+ },
323
+ };
324
+ };
325
+ export const CalendarEventOperations = {
326
+ CalendarEventOperations_listCalendarEvents: async (context, ...args) => {
327
+ const event = args[0];
328
+ const accountConfigId = getAccountConfigIdFromEvent(event);
329
+ const query = context.request.query;
330
+ const window = readWindow(query.from, query.to);
331
+ if (!window.ok)
332
+ return badRequest(window.error);
333
+ const deps = calendarDepsOf(await getClient());
334
+ const collections = await collectionsForListing(deps, accountConfigId, readCalendarIds(query.calendarId));
335
+ if (!collections.ok)
336
+ return answerRefusal(collections.error);
337
+ if (query.calendarObjectId) {
338
+ const located = await locateResource(deps, collections.value, query.calendarObjectId);
339
+ if (!located)
340
+ return { items: [] };
341
+ const own = await listObjectInstances(deps, located.collection, located.object, window.value);
342
+ return { items: own.map(toInstanceResponse) };
343
+ }
344
+ const instances = await listCalendarInstances(deps, collections.value, window.value);
345
+ return { items: instances.map(toInstanceResponse) };
346
+ },
347
+ CalendarEventOperations_createCalendarEvent: async (context, ...args) => {
348
+ const event = args[0];
349
+ const accountConfigId = getAccountConfigIdFromEvent(event);
350
+ const input = context.request.requestBody;
351
+ const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
352
+ const created = await createCalendarEventFor(deps, accountConfigId, input);
353
+ if (!created.ok)
354
+ return answerRefusal(created.error);
355
+ return toEventResponse(created.value);
356
+ },
357
+ };
358
+ export const CalendarEventDetailOperations = {
359
+ CalendarEventDetailOperations_getCalendarEvent: async (context, ...args) => {
360
+ const event = args[0];
361
+ const accountConfigId = getAccountConfigIdFromEvent(event);
362
+ const request = scopedRequestOf(context);
363
+ if (!request.ok)
364
+ return badRequest(request.error);
365
+ const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
366
+ if (request.value.calendarId === "") {
367
+ const located = await locateResource(deps, await listCalendarsFor(deps, accountConfigId), request.value.calendarObjectId);
368
+ if (!located) {
369
+ return notFound(`no event ${request.value.calendarObjectId} on this account`);
370
+ }
371
+ return toEventResponse(located.object);
372
+ }
373
+ const resolved = await resolveResource(deps, accountConfigId, request.value, "read");
374
+ if (!resolved.ok)
375
+ return answerRefusal(resolved.error);
376
+ return toEventResponse(resolved.value.object);
377
+ },
378
+ CalendarEventDetailOperations_updateCalendarEvent: async (context, ...args) => {
379
+ const event = args[0];
380
+ const accountConfigId = getAccountConfigIdFromEvent(event);
381
+ const body = context.request
382
+ .requestBody;
383
+ const request = scopedRequestOf(context);
384
+ if (!request.ok)
385
+ return badRequest(request.error);
386
+ const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
387
+ const updated = await updateCalendarEventFor(deps, accountConfigId, request.value, pickEventUpdate(body));
388
+ if (!updated.ok)
389
+ return answerRefusal(updated.error);
390
+ if (!updated.value) {
391
+ throw new Error("a scoped update resolved to a delete");
392
+ }
393
+ return toEventResponse(updated.value);
394
+ },
395
+ CalendarEventDetailOperations_deleteCalendarEvent: async (context, ...args) => {
396
+ const event = args[0];
397
+ const accountConfigId = getAccountConfigIdFromEvent(event);
398
+ const request = scopedRequestOf(context);
399
+ if (!request.ok)
400
+ return badRequest(request.error);
401
+ const deps = calendarEventDepsOf(calendarDepsOf(await getClient()));
402
+ const removed = await deleteCalendarEventFor(deps, accountConfigId, request.value);
403
+ if (!removed.ok)
404
+ return answerRefusal(removed.error);
405
+ return { statusCode: 204 };
406
+ },
407
+ };
408
+ export const CalendarFreeBusyOperations = {
409
+ CalendarFreeBusyOperations_listCalendarFreeBusy: async (context, ...args) => {
410
+ const event = args[0];
411
+ const accountConfigId = getAccountConfigIdFromEvent(event);
412
+ const query = context.request.query;
413
+ const window = readWindow(query.from, query.to);
414
+ if (!window.ok)
415
+ return badRequest(window.error);
416
+ const deps = calendarDepsOf(await getClient());
417
+ const collections = await listCalendarsFor(deps, accountConfigId);
418
+ const spans = await listBusySpans(deps, collections, window.value);
419
+ const items = spans.map((span) => ({
420
+ start: toOffsetIso(span.startMs, "UTC"),
421
+ end: toOffsetIso(span.endMs, "UTC"),
422
+ }));
423
+ return { items };
424
+ },
425
+ };
@@ -0,0 +1,47 @@
1
+ import type { CalendarFeedResponse, CalendarFeedStateResponse } from "@remit/api-openapi-types";
2
+ import { type RawApiResponse } from "../response.js";
3
+ import type { CalendarDetailOperationIds, CalendarFeedDetailOperationIds, CalendarFeedOperationIds, OperationHandler } from "../types.js";
4
+ import { type CalendarDeps, type CalendarOutcome } from "./calendar.js";
5
+ /**
6
+ * Mints the calendar's feed address, or replaces the one it has.
7
+ *
8
+ * The plaintext exists here and nowhere else: the store keeps a digest, so this
9
+ * return value is the only chance anybody has to read the token. Ownership is
10
+ * settled before the mint — a calendar the caller does not hold is absent, not
11
+ * forbidden, which is the same answer `getCalendar` gives it.
12
+ */
13
+ export declare const putCalendarFeedFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string) => Promise<CalendarOutcome<CalendarFeedResponse>>;
14
+ /** Whether the calendar has a feed address, and how old it is. */
15
+ export declare const findCalendarFeedFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string) => Promise<CalendarOutcome<CalendarFeedStateResponse>>;
16
+ /**
17
+ * Revokes the calendar's feed address.
18
+ *
19
+ * A calendar with no feed is refused rather than answered 204: the caller asked
20
+ * for a subscription to stop working, and reporting success for one that was
21
+ * never there reads as confirmation that it has.
22
+ */
23
+ export declare const revokeCalendarFeedFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string) => Promise<CalendarOutcome<null>>;
24
+ /**
25
+ * Serves a calendar to whoever holds its secret address.
26
+ *
27
+ * The token is the whole credential, so a value that cannot be one is refused
28
+ * before anything is hashed, and the digest of what arrived is what the store is
29
+ * asked about — the plaintext is never compared against a stored one because
30
+ * none is stored.
31
+ *
32
+ * `Last-Modified` is the collection's own timestamp, not the newest event in
33
+ * it: an event deleted from a calendar leaves every survivor older than the
34
+ * change, and a validator that moves backwards tells a subscriber its cached
35
+ * copy is still good. The collection is stamped by the same sequence bump every
36
+ * write and every delete goes through, so it only ever moves forward.
37
+ *
38
+ * A row whose collection is gone is the same 404. It should not exist — a
39
+ * calendar's delete takes its token with it, in one batch — and serving a
40
+ * blank calendar for one would be worse than saying nothing.
41
+ */
42
+ export declare const serveCalendarFeed: (deps: CalendarDeps, feedToken: string, conditions: {
43
+ ifNoneMatch: string | undefined;
44
+ ifModifiedSince: string | undefined;
45
+ }) => Promise<RawApiResponse>;
46
+ export declare const CalendarFeedDetailOperations: Record<CalendarFeedDetailOperationIds, OperationHandler<CalendarDetailOperationIds>>;
47
+ export declare const CalendarFeedOperations: Record<CalendarFeedOperationIds, OperationHandler<CalendarFeedOperationIds>>;