@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,685 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, describe, it } from "node:test";
3
- import type { APIGatewayProxyEvent } from "aws-lambda";
4
- import type { Context } from "openapi-backend";
5
- import { deriveAccountConfigId } from "../auth.js";
6
- import { authenticateSelfHostRequest } from "../jwt-auth.js";
7
- import {
8
- _resetForTest,
9
- type RemitClient,
10
- setClient,
11
- } from "../service/data-client.js";
12
- import { CalendarOperations } from "./calendar.js";
13
- import {
14
- CalendarEventDetailOperations,
15
- CalendarEventOperations,
16
- CalendarFreeBusyOperations,
17
- } from "./calendar-event.js";
18
- import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
19
-
20
- /**
21
- * The event, free/busy and window wrappers driven the way an HTTP request
22
- * drives them — through the registered client — against the SQLite store the
23
- * self-host build ships (issue #1033).
24
- */
25
-
26
- type Handler = (
27
- context: Context,
28
- event: APIGatewayProxyEvent,
29
- ) => Promise<Record<string, unknown>>;
30
-
31
- const listCalendars =
32
- CalendarOperations.CalendarOperations_listCalendars as Handler;
33
- const createCalendar =
34
- CalendarOperations.CalendarOperations_createCalendar as Handler;
35
- const listEvents =
36
- CalendarEventOperations.CalendarEventOperations_listCalendarEvents as Handler;
37
- const createEvent =
38
- CalendarEventOperations.CalendarEventOperations_createCalendarEvent as Handler;
39
- const getEvent =
40
- CalendarEventDetailOperations.CalendarEventDetailOperations_getCalendarEvent as Handler;
41
- const updateEvent =
42
- CalendarEventDetailOperations.CalendarEventDetailOperations_updateCalendarEvent as Handler;
43
- const deleteEvent =
44
- CalendarEventDetailOperations.CalendarEventDetailOperations_deleteCalendarEvent as Handler;
45
- const listFreeBusy =
46
- CalendarFreeBusyOperations.CalendarFreeBusyOperations_listCalendarFreeBusy as Handler;
47
-
48
- interface Instance {
49
- calendarId: string;
50
- calendarObjectId: string;
51
- recurrenceId: string;
52
- summary: string;
53
- start: string;
54
- end: string;
55
- }
56
-
57
- interface Span {
58
- start: string;
59
- end: string;
60
- }
61
-
62
- const WINDOW = { from: "2026-09-01T00:00:00Z", to: "2026-10-31T00:00:00Z" };
63
-
64
- let client: RemitClient;
65
- let cleanup: () => void;
66
- let minted = 0;
67
-
68
- const contextOf = (request: {
69
- params?: Record<string, string>;
70
- query?: Record<string, unknown>;
71
- headers?: Record<string, string>;
72
- requestBody?: unknown;
73
- }): Context => ({ request }) as unknown as Context;
74
-
75
- const eventOf = (sub: string): APIGatewayProxyEvent =>
76
- ({
77
- requestContext: { authorizer: { claims: { sub } } },
78
- }) as unknown as APIGatewayProxyEvent;
79
-
80
- /** A caller nobody else in this file shares a calendar with. */
81
- const anAccount = (): { sub: string; event: APIGatewayProxyEvent } => {
82
- minted += 1;
83
- const sub = `calendar-event-sub-${minted}`;
84
- return { sub, event: eventOf(sub) };
85
- };
86
-
87
- const defaultCalendarId = async (
88
- event: APIGatewayProxyEvent,
89
- ): Promise<string> => {
90
- const listed = (await listEvents(
91
- contextOf({ query: WINDOW }),
92
- event,
93
- )) as unknown as { items: Instance[] };
94
- assert.ok(Array.isArray(listed.items));
95
- const [collection] = await client.calendarCollection.listByAccountConfig(
96
- deriveAccountConfigId(
97
- (event.requestContext.authorizer as { claims: { sub: string } }).claims
98
- .sub,
99
- ),
100
- );
101
- assert.ok(collection);
102
- return collection.calendarId;
103
- };
104
-
105
- const seedEvent = async (
106
- event: APIGatewayProxyEvent,
107
- input: Record<string, unknown>,
108
- ): Promise<{ calendarObjectId: string; etag: string; icalData: string }> => {
109
- const created = await createEvent(contextOf({ requestBody: input }), event);
110
- assert.equal(
111
- created.statusCode,
112
- undefined,
113
- `create was refused: ${JSON.stringify(created)}`,
114
- );
115
- return created as unknown as {
116
- calendarObjectId: string;
117
- etag: string;
118
- icalData: string;
119
- };
120
- };
121
-
122
- before(async () => {
123
- _resetForTest();
124
- ({ client, cleanup } = await createCalendarSqliteClient());
125
- setClient(client);
126
- });
127
-
128
- after(() => {
129
- _resetForTest();
130
- cleanup();
131
- });
132
-
133
- describe("an unauthenticated calendar request", () => {
134
- const routes = [
135
- { httpMethod: "GET", path: "/calendars" },
136
- { httpMethod: "GET", path: "/calendar-events" },
137
- { httpMethod: "POST", path: "/calendar-events" },
138
- { httpMethod: "GET", path: "/calendar-free-busy" },
139
- { httpMethod: "GET", path: "/calendar-suggestions" },
140
- ];
141
-
142
- for (const route of routes) {
143
- it(`is answered 401 before ${route.httpMethod} ${route.path} runs`, async () => {
144
- const refusal = await authenticateSelfHostRequest({
145
- ...route,
146
- headers: {},
147
- } as unknown as APIGatewayProxyEvent);
148
-
149
- assert.equal(refusal?.statusCode, 401);
150
- });
151
- }
152
-
153
- it("never serves a calendar to a request carrying no claims", async () => {
154
- // The gate above is what a browser meets. This is the second lock: a
155
- // listing reached with no identity refuses rather than falling back to
156
- // somebody's account.
157
- await assert.rejects(
158
- () =>
159
- listEvents(
160
- contextOf({ query: WINDOW }),
161
- {} as unknown as APIGatewayProxyEvent,
162
- ),
163
- /Missing accountConfigId/,
164
- );
165
- });
166
- });
167
-
168
- describe("GET /calendar-events", () => {
169
- it("covers every collection the caller holds when it names none", async () => {
170
- const { event } = anAccount();
171
- const defaultId = await defaultCalendarId(event);
172
- const work = (await createCalendar(
173
- contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
174
- event,
175
- )) as unknown as { calendarId: string };
176
- await seedEvent(event, {
177
- calendarId: defaultId,
178
- summary: "Dentist",
179
- start: "2026-09-07T09:00:00Z",
180
- end: "2026-09-07T10:00:00Z",
181
- });
182
- await seedEvent(event, {
183
- calendarId: work.calendarId,
184
- summary: "Stand-up",
185
- start: "2026-09-08T09:00:00Z",
186
- end: "2026-09-08T09:15:00Z",
187
- });
188
-
189
- const listed = (await listEvents(
190
- contextOf({ query: WINDOW }),
191
- event,
192
- )) as unknown as { items: Instance[] };
193
-
194
- assert.deepEqual(
195
- listed.items.map((instance) => instance.summary),
196
- ["Dentist", "Stand-up"],
197
- );
198
- });
199
-
200
- it("covers only the subset it names", async () => {
201
- const { event } = anAccount();
202
- const defaultId = await defaultCalendarId(event);
203
- const work = (await createCalendar(
204
- contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
205
- event,
206
- )) as unknown as { calendarId: string };
207
- await seedEvent(event, {
208
- calendarId: defaultId,
209
- summary: "Dentist",
210
- start: "2026-09-07T09:00:00Z",
211
- end: "2026-09-07T10:00:00Z",
212
- });
213
- await seedEvent(event, {
214
- calendarId: work.calendarId,
215
- summary: "Stand-up",
216
- start: "2026-09-08T09:00:00Z",
217
- end: "2026-09-08T09:15:00Z",
218
- });
219
-
220
- const listed = (await listEvents(
221
- contextOf({ query: { ...WINDOW, calendarId: [work.calendarId] } }),
222
- event,
223
- )) as unknown as { items: Instance[] };
224
-
225
- assert.deepEqual(
226
- listed.items.map((instance) => instance.summary),
227
- ["Stand-up"],
228
- );
229
- });
230
-
231
- it("answers not-found for a collection on another account rather than an empty day", async () => {
232
- // An empty list here would read as "you have nothing on", which is the one
233
- // answer a clash check must never get wrong.
234
- const stranger = anAccount();
235
- const strangersCalendarId = await defaultCalendarId(stranger.event);
236
- const { event } = anAccount();
237
-
238
- const listed = await listEvents(
239
- contextOf({ query: { ...WINDOW, calendarId: [strangersCalendarId] } }),
240
- event,
241
- );
242
-
243
- assert.equal(listed.statusCode, 404);
244
- assert.equal((listed.body as { code: string }).code, "not_found");
245
- });
246
-
247
- it("refuses a window that runs backwards or covers more than a year", async () => {
248
- const { event } = anAccount();
249
-
250
- const backwards = await listEvents(
251
- contextOf({
252
- query: { from: "2026-09-08T00:00:00Z", to: "2026-09-07T00:00:00Z" },
253
- }),
254
- event,
255
- );
256
- const tooWide = await listEvents(
257
- contextOf({
258
- query: { from: "2026-01-01T00:00:00Z", to: "2027-06-01T00:00:00Z" },
259
- }),
260
- event,
261
- );
262
-
263
- assert.equal(backwards.statusCode, 400);
264
- assert.equal((backwards.body as { code: string }).code, "invalid_window");
265
- assert.equal(tooWide.statusCode, 400);
266
- });
267
- });
268
-
269
- describe("GET /calendar-events?calendarObjectId", () => {
270
- it("lists the occurrences of that one resource, across the caller's calendars", async () => {
271
- const { event } = anAccount();
272
- const defaultId = await defaultCalendarId(event);
273
- const work = (await createCalendar(
274
- contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
275
- event,
276
- )) as unknown as { calendarId: string };
277
- const series = await seedEvent(event, {
278
- calendarId: work.calendarId,
279
- summary: "Stand-up",
280
- start: "2026-09-07T09:00:00Z",
281
- end: "2026-09-07T09:15:00Z",
282
- recurrenceRule: "FREQ=WEEKLY;COUNT=3",
283
- });
284
- await seedEvent(event, {
285
- calendarId: defaultId,
286
- summary: "Dentist",
287
- start: "2026-09-08T09:00:00Z",
288
- end: "2026-09-08T10:00:00Z",
289
- });
290
-
291
- const listed = (await listEvents(
292
- contextOf({
293
- query: { ...WINDOW, calendarObjectId: series.calendarObjectId },
294
- }),
295
- event,
296
- )) as unknown as { items: Instance[] };
297
-
298
- assert.deepEqual(
299
- listed.items.map((instance) => [
300
- instance.calendarId,
301
- instance.start.slice(0, 10),
302
- ]),
303
- [
304
- [work.calendarId, "2026-09-07"],
305
- [work.calendarId, "2026-09-14"],
306
- [work.calendarId, "2026-09-21"],
307
- ],
308
- );
309
- });
310
-
311
- it("lists nothing for a resource on another account", async () => {
312
- const stranger = anAccount();
313
- const theirs = await seedEvent(stranger.event, {
314
- calendarId: await defaultCalendarId(stranger.event),
315
- summary: "Private",
316
- start: "2026-09-07T09:00:00Z",
317
- end: "2026-09-07T10:00:00Z",
318
- });
319
- const { event } = anAccount();
320
-
321
- const listed = (await listEvents(
322
- contextOf({
323
- query: { ...WINDOW, calendarObjectId: theirs.calendarObjectId },
324
- }),
325
- event,
326
- )) as unknown as { items: Instance[] };
327
-
328
- assert.deepEqual(listed.items, []);
329
- });
330
- });
331
-
332
- describe("GET /calendar-events/{calendarObjectId} without a calendar", () => {
333
- it("finds the resource among the caller's calendars", async () => {
334
- const { event } = anAccount();
335
- await defaultCalendarId(event);
336
- const work = (await createCalendar(
337
- contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
338
- event,
339
- )) as unknown as { calendarId: string };
340
- const seeded = await seedEvent(event, {
341
- calendarId: work.calendarId,
342
- summary: "Stand-up",
343
- start: "2026-09-07T09:00:00Z",
344
- end: "2026-09-07T09:15:00Z",
345
- });
346
-
347
- const found = (await getEvent(
348
- contextOf({
349
- params: { calendarObjectId: seeded.calendarObjectId },
350
- query: {},
351
- }),
352
- event,
353
- )) as unknown as { calendarId: string; dtStart: string };
354
-
355
- assert.equal(found.calendarId, work.calendarId);
356
- assert.match(found.dtStart, /^2026-09-07/);
357
- });
358
-
359
- it("answers not-found for a resource on another account", async () => {
360
- const stranger = anAccount();
361
- const theirs = await seedEvent(stranger.event, {
362
- calendarId: await defaultCalendarId(stranger.event),
363
- summary: "Private",
364
- start: "2026-09-07T09:00:00Z",
365
- end: "2026-09-07T10:00:00Z",
366
- });
367
- const { event } = anAccount();
368
-
369
- const found = await getEvent(
370
- contextOf({
371
- params: { calendarObjectId: theirs.calendarObjectId },
372
- query: {},
373
- }),
374
- event,
375
- );
376
-
377
- assert.equal(found.statusCode, 404);
378
- });
379
- });
380
-
381
- describe("POST /calendar-events", () => {
382
- it("refuses a recurrence rule it cannot read and writes nothing", async () => {
383
- const { event } = anAccount();
384
- const calendarId = await defaultCalendarId(event);
385
-
386
- const created = await createEvent(
387
- contextOf({
388
- requestBody: {
389
- calendarId,
390
- summary: "Every other Tuesday",
391
- start: "2026-09-07T09:00:00Z",
392
- end: "2026-09-07T10:00:00Z",
393
- recurrenceRule: "every other tuesday",
394
- },
395
- }),
396
- event,
397
- );
398
-
399
- assert.equal(created.statusCode, 400);
400
- assert.equal(
401
- (created.body as { code: string }).code,
402
- "invalid_recurrence_rule",
403
- );
404
- assert.deepEqual(
405
- await client.calendarObject.listByCalendar(calendarId),
406
- [],
407
- "the refused create left the calendar empty",
408
- );
409
- });
410
-
411
- it("refuses a time zone this server cannot resolve", async () => {
412
- // A Windows zone name is what a client that has not normalised its input
413
- // sends. Storing it would draw the event hours from where it belongs.
414
- const { event } = anAccount();
415
- const calendarId = await defaultCalendarId(event);
416
-
417
- const created = await createEvent(
418
- contextOf({
419
- requestBody: {
420
- calendarId,
421
- summary: "Review",
422
- start: "2026-09-07T09:00:00+02:00",
423
- end: "2026-09-07T10:00:00+02:00",
424
- timeZone: "Pacific Standard Time",
425
- },
426
- }),
427
- event,
428
- );
429
-
430
- assert.equal(created.statusCode, 400);
431
- assert.equal((created.body as { code: string }).code, "unknown_time_zone");
432
- assert.deepEqual(
433
- await client.calendarObject.listByCalendar(calendarId),
434
- [],
435
- );
436
- });
437
- });
438
-
439
- describe("PATCH /calendar-events/{calendarObjectId}?scope=This", () => {
440
- it("writes a RECURRENCE-ID override and leaves the rest of the series alone", async () => {
441
- const { event } = anAccount();
442
- const calendarId = await defaultCalendarId(event);
443
- const created = await seedEvent(event, {
444
- calendarId,
445
- summary: "Stand-up",
446
- start: "2026-09-07T09:00:00Z",
447
- end: "2026-09-07T09:15:00Z",
448
- recurrenceRule: "FREQ=WEEKLY;COUNT=5",
449
- });
450
-
451
- const updated = await updateEvent(
452
- contextOf({
453
- params: { calendarObjectId: created.calendarObjectId },
454
- query: {
455
- calendarId,
456
- scope: "This",
457
- recurrenceId: "2026-09-21T09:00:00Z",
458
- },
459
- requestBody: { summary: "Stand-up (in the big room)" },
460
- }),
461
- event,
462
- );
463
-
464
- assert.equal(
465
- updated.statusCode,
466
- undefined,
467
- `the scoped update was refused: ${JSON.stringify(updated)}`,
468
- );
469
- assert.match(
470
- String(updated.icalData),
471
- /RECURRENCE-ID[^\r\n]*:20260921T090000Z/,
472
- "the resource carries an override for the occurrence that was edited",
473
- );
474
-
475
- const listed = (await listEvents(
476
- contextOf({ query: WINDOW }),
477
- event,
478
- )) as unknown as { items: Instance[] };
479
- const renamed = listed.items.filter(
480
- (instance) => instance.summary === "Stand-up (in the big room)",
481
- );
482
- assert.equal(listed.items.length, 5, "the series still has five drawings");
483
- assert.deepEqual(
484
- renamed.map((instance) => instance.start),
485
- ["2026-09-21T09:00:00+00:00"],
486
- "exactly the named occurrence took the new name",
487
- );
488
- });
489
- });
490
-
491
- describe("DELETE /calendar-events/{calendarObjectId}", () => {
492
- it("refuses a delete built on an etag the resource no longer carries", async () => {
493
- const { event } = anAccount();
494
- const calendarId = await defaultCalendarId(event);
495
- const created = await seedEvent(event, {
496
- calendarId,
497
- summary: "Stand-up",
498
- start: "2026-09-07T09:00:00Z",
499
- end: "2026-09-07T09:15:00Z",
500
- });
501
- await updateEvent(
502
- contextOf({
503
- params: { calendarObjectId: created.calendarObjectId },
504
- query: { calendarId },
505
- requestBody: { summary: "Stand-up (renamed)" },
506
- }),
507
- event,
508
- );
509
-
510
- const removed = await deleteEvent(
511
- contextOf({
512
- params: { calendarObjectId: created.calendarObjectId },
513
- query: { calendarId },
514
- headers: { "If-Match": `"${created.etag}"` },
515
- }),
516
- event,
517
- );
518
-
519
- assert.equal(removed.statusCode, 412);
520
- assert.equal(
521
- (removed.body as { code: string }).code,
522
- "precondition_failed",
523
- );
524
- const survivor = await client.calendarObject.find(
525
- calendarId,
526
- created.calendarObjectId,
527
- );
528
- assert.equal(
529
- survivor?.summary,
530
- "Stand-up (renamed)",
531
- "the refused delete left the other writer's version in place",
532
- );
533
- });
534
-
535
- it("lets the delete through once the caller has read the current etag", async () => {
536
- const { event } = anAccount();
537
- const calendarId = await defaultCalendarId(event);
538
- const created = await seedEvent(event, {
539
- calendarId,
540
- summary: "Stand-up",
541
- start: "2026-09-07T09:00:00Z",
542
- end: "2026-09-07T09:15:00Z",
543
- });
544
-
545
- const removed = await deleteEvent(
546
- contextOf({
547
- params: { calendarObjectId: created.calendarObjectId },
548
- query: { calendarId },
549
- headers: { "if-match": `W/"${created.etag}"` },
550
- }),
551
- event,
552
- );
553
-
554
- assert.equal(removed.statusCode, 204);
555
- assert.equal(
556
- await client.calendarObject.find(calendarId, created.calendarObjectId),
557
- null,
558
- );
559
- });
560
- });
561
-
562
- describe("GET /calendar-free-busy", () => {
563
- it("merges overlapping meetings across two collections into one busy stretch", async () => {
564
- const { event } = anAccount();
565
- const defaultId = await defaultCalendarId(event);
566
- const work = (await createCalendar(
567
- contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
568
- event,
569
- )) as unknown as { calendarId: string };
570
- await seedEvent(event, {
571
- calendarId: defaultId,
572
- summary: "Dentist",
573
- start: "2026-09-07T09:00:00Z",
574
- end: "2026-09-07T10:00:00Z",
575
- });
576
- await seedEvent(event, {
577
- calendarId: work.calendarId,
578
- summary: "Review",
579
- start: "2026-09-07T09:30:00Z",
580
- end: "2026-09-07T11:00:00Z",
581
- });
582
-
583
- const busy = (await listFreeBusy(
584
- contextOf({ query: WINDOW }),
585
- event,
586
- )) as unknown as { items: Span[] };
587
-
588
- assert.deepEqual(busy.items, [
589
- { start: "2026-09-07T09:00:00+00:00", end: "2026-09-07T11:00:00+00:00" },
590
- ]);
591
- });
592
-
593
- it("renders every span with an explicit +00:00 offset", async () => {
594
- // A busy span is an interval on the clock, not a civil date, so it never
595
- // carries a calendar's local zone — a caller renders it in whichever zone
596
- // it is drawing.
597
- const { event } = anAccount();
598
- const calendarId = await defaultCalendarId(event);
599
- await client.calendarCollection.update(
600
- deriveAccountConfigId(
601
- (event.requestContext.authorizer as { claims: { sub: string } }).claims
602
- .sub,
603
- ),
604
- calendarId,
605
- { timezone: "America/New_York" },
606
- );
607
- await seedEvent(event, {
608
- calendarId,
609
- summary: "Dentist",
610
- start: "2026-09-07T09:00:00Z",
611
- end: "2026-09-07T10:00:00Z",
612
- });
613
-
614
- const busy = (await listFreeBusy(
615
- contextOf({ query: WINDOW }),
616
- event,
617
- )) as unknown as { items: Span[] };
618
-
619
- assert.deepEqual(busy.items, [
620
- { start: "2026-09-07T09:00:00+00:00", end: "2026-09-07T10:00:00+00:00" },
621
- ]);
622
- });
623
-
624
- it("leaves out an event that was never busy time", async () => {
625
- const { event } = anAccount();
626
- const calendarId = await defaultCalendarId(event);
627
- await seedEvent(event, {
628
- calendarId,
629
- summary: "Focus block",
630
- start: "2026-09-07T09:00:00Z",
631
- end: "2026-09-07T10:00:00Z",
632
- transparency: "Transparent",
633
- });
634
- await seedEvent(event, {
635
- calendarId,
636
- summary: "Cancelled review",
637
- start: "2026-09-07T11:00:00Z",
638
- end: "2026-09-07T12:00:00Z",
639
- status: "Cancelled",
640
- });
641
-
642
- const busy = (await listFreeBusy(
643
- contextOf({ query: WINDOW }),
644
- event,
645
- )) as unknown as { items: Span[] };
646
-
647
- assert.deepEqual(busy.items, []);
648
- });
649
-
650
- it("refuses the same windows the event listing refuses", async () => {
651
- const { event } = anAccount();
652
-
653
- const missing = await listFreeBusy(
654
- contextOf({ query: { from: "2026-09-07T00:00:00Z" } }),
655
- event,
656
- );
657
- const tooWide = await listFreeBusy(
658
- contextOf({
659
- query: { from: "2026-01-01T00:00:00Z", to: "2027-06-01T00:00:00Z" },
660
- }),
661
- event,
662
- );
663
-
664
- assert.equal(missing.statusCode, 400);
665
- assert.equal((missing.body as { code: string }).code, "invalid_window");
666
- assert.equal(tooWide.statusCode, 400);
667
- assert.equal((tooWide.body as { code: string }).code, "invalid_window");
668
- });
669
- });
670
-
671
- describe("GET /calendars", () => {
672
- it("provisions the account's default collection on a first read", async () => {
673
- const { event } = anAccount();
674
-
675
- const listed = (await listCalendars(contextOf({}), event)) as unknown as {
676
- items: Array<{ urlSegment: string; source: string }>;
677
- };
678
-
679
- assert.deepEqual(
680
- listed.items.map((item) => item.urlSegment),
681
- ["default"],
682
- );
683
- assert.equal(listed.items[0]?.source, "Default");
684
- });
685
- });