@remit/backend 0.0.143 → 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 -522
  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
@@ -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
- });