@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,336 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { randomUUID } from "node:crypto";
3
- import { createServer, type Server } from "node:http";
4
- import type { AddressInfo } from "node:net";
5
- import { after, before, beforeEach, describe, it } from "node:test";
6
- import type {
7
- AccountResponse,
8
- AccountService as AccountServiceName,
9
- } from "@remit/api-openapi-types";
10
- import { AccountService } from "@remit/domain-enums";
11
- import { MICROSOFT_SERVICE_SCOPES } from "@remit/mail-oauth-service";
12
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
13
- import { deriveAccountConfigId } from "../auth.js";
14
- import {
15
- _resetForTest,
16
- type RemitClient,
17
- setClient,
18
- } from "../service/data-client.js";
19
- import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
20
-
21
- const CLIENT_SECRET = "consent-test-client-secret";
22
- const WEB_ORIGIN = "https://mail.example.com";
23
- const GRAPH = "https://graph.microsoft.com/";
24
- const MAIL_SCOPES = [...MICROSOFT_SERVICE_SCOPES[AccountService.Mail]];
25
- const CALENDAR_SCOPES = [...MICROSOFT_SERVICE_SCOPES[AccountService.Calendar]];
26
-
27
- let handler: (
28
- event: APIGatewayProxyEvent,
29
- context: unknown,
30
- ) => Promise<APIGatewayProxyResult>;
31
- let client: RemitClient;
32
- let cleanup: () => void;
33
- let tokenEndpoint: Server;
34
- let withheld: Set<string>;
35
- let redemptions: string[][];
36
-
37
- const jwt = (claims: Record<string, unknown>): string =>
38
- [
39
- Buffer.from(JSON.stringify({ alg: "RS256" })).toString("base64url"),
40
- Buffer.from(JSON.stringify(claims), "utf8").toString("base64url"),
41
- "signature-not-checked-here",
42
- ].join(".");
43
-
44
- const readBody = async (request: AsyncIterable<Buffer>): Promise<string> => {
45
- const chunks: Buffer[] = [];
46
- for await (const chunk of request) chunks.push(chunk);
47
- return Buffer.concat(chunks).toString("utf8");
48
- };
49
-
50
- const startTokenEndpoint = async (): Promise<string> => {
51
- tokenEndpoint = createServer(async (request, response) => {
52
- const form = new URLSearchParams(await readBody(request));
53
- const requested = (form.get("scope") ?? "").split(" ").filter(Boolean);
54
- redemptions.push(requested);
55
- const granted = requested.filter((scope) => !withheld.has(scope));
56
- const resourceScopes = granted.filter((scope) => scope.includes("://"));
57
- if (resourceScopes.length === 0) {
58
- response.writeHead(400, { "Content-Type": "application/json" });
59
- response.end(
60
- JSON.stringify({ error: "invalid_grant", error_codes: [65001] }),
61
- );
62
- return;
63
- }
64
- response.writeHead(200, { "Content-Type": "application/json" });
65
- response.end(
66
- JSON.stringify({
67
- access_token: "access-token",
68
- refresh_token: `refresh-token-${redemptions.length}`,
69
- id_token: jwt({ preferred_username: "person@example.com" }),
70
- expires_in: 3600,
71
- scope: granted
72
- .map((scope) =>
73
- scope.startsWith(GRAPH) ? scope.slice(GRAPH.length) : scope,
74
- )
75
- .join(" "),
76
- }),
77
- );
78
- });
79
- await new Promise<void>((resolve) => tokenEndpoint.listen(0, resolve));
80
- const { port } = tokenEndpoint.address() as AddressInfo;
81
- return `http://127.0.0.1:${port}/token`;
82
- };
83
-
84
- const headerOf = (
85
- response: APIGatewayProxyResult,
86
- name: string,
87
- ): string | undefined => {
88
- for (const [key, value] of Object.entries(response.headers ?? {})) {
89
- if (key.toLowerCase() === name) return String(value);
90
- }
91
- return undefined;
92
- };
93
-
94
- const request = (
95
- sub: string,
96
- httpMethod: string,
97
- path: string,
98
- body: unknown,
99
- queryStringParameters: Record<string, string> | null = null,
100
- ): Promise<APIGatewayProxyResult> =>
101
- handler(
102
- {
103
- httpMethod,
104
- path,
105
- headers: { "content-type": "application/json" },
106
- queryStringParameters,
107
- body: body === null ? null : JSON.stringify(body),
108
- requestContext: { authorizer: { claims: { sub } } },
109
- } as unknown as APIGatewayProxyEvent,
110
- { awsRequestId: `req-${randomUUID()}`, functionName: "test" },
111
- );
112
-
113
- const startConsent = async (
114
- sub: string,
115
- services: AccountServiceName[] | undefined,
116
- ): Promise<URL> => {
117
- const response = await request(
118
- sub,
119
- "POST",
120
- "/accounts/oauth/microsoft/start",
121
- {
122
- email: "person@example.com",
123
- services,
124
- },
125
- );
126
- assert.equal(response.statusCode, 200, response.body);
127
- return new URL(
128
- (JSON.parse(response.body) as { authorizationUrl: string })
129
- .authorizationUrl,
130
- );
131
- };
132
-
133
- const consent = async (
134
- sub: string,
135
- services: AccountServiceName[] | undefined,
136
- ): Promise<string> => {
137
- const authorizationUrl = await startConsent(sub, services);
138
- const state = authorizationUrl.searchParams.get("state");
139
- assert.ok(state, "the authorization URL carries the signed state");
140
- const response = await request(
141
- sub,
142
- "GET",
143
- "/accounts/oauth/microsoft/callback",
144
- null,
145
- { code: "auth-code", state },
146
- );
147
- assert.equal(response.statusCode, 302, response.body);
148
- const location = headerOf(response, "location");
149
- assert.ok(location, "the callback answers with a Location");
150
- return location;
151
- };
152
-
153
- const onlyAccount = async (sub: string) => {
154
- const accounts = await client.account.listAllByAccountConfig(
155
- deriveAccountConfigId(sub),
156
- );
157
- assert.equal(accounts.length, 1, "the consent wrote exactly one account");
158
- return accounts[0];
159
- };
160
-
161
- const patchServices = (
162
- sub: string,
163
- accountId: string,
164
- syncedServices: AccountServiceName[],
165
- ): Promise<APIGatewayProxyResult> =>
166
- request(sub, "PATCH", `/accounts/${accountId}`, { syncedServices });
167
-
168
- before(async () => {
169
- _resetForTest();
170
- process.env.DATA_BACKEND = "sqlite";
171
- process.env.CORS_ALLOWED_ORIGINS = WEB_ORIGIN;
172
- process.env.MSOAUTH_REDIRECT_URI = `${WEB_ORIGIN}/accounts/oauth/microsoft/callback`;
173
- process.env.MSOAUTH_CLIENT_ID = "consent-test-client-id";
174
- process.env.MSOAUTH_CLIENT_SECRET = CLIENT_SECRET;
175
- process.env.MSOAUTH_TOKEN_ENDPOINT = await startTokenEndpoint();
176
- process.env.FAKE_KMS_DATAKEY = "0".repeat(64);
177
- process.env.AWS_REGION = "eu-west-1";
178
- process.env.AWS_ACCESS_KEY_ID = "test";
179
- process.env.AWS_SECRET_ACCESS_KEY = "test";
180
- ({ client, cleanup } = await createCalendarSqliteClient());
181
- setClient(client);
182
- ({ handler } = (await import("../index.js")) as unknown as {
183
- handler: typeof handler;
184
- });
185
- });
186
-
187
- beforeEach(() => {
188
- withheld = new Set();
189
- redemptions = [];
190
- });
191
-
192
- after(async () => {
193
- _resetForTest();
194
- cleanup();
195
- await new Promise<void>((resolve) => tokenEndpoint.close(() => resolve()));
196
- });
197
-
198
- describe("the Microsoft consent for the services an account syncs", () => {
199
- it("asks for the calendar scope only when the start names Calendar", async () => {
200
- const sub = randomUUID();
201
-
202
- const mailOnly = (
203
- await startConsent(sub, [AccountService.Mail])
204
- ).searchParams
205
- .get("scope")
206
- ?.split(" ");
207
- const calendarOnly = (
208
- await startConsent(sub, [AccountService.Calendar])
209
- ).searchParams
210
- .get("scope")
211
- ?.split(" ");
212
-
213
- assert.ok(mailOnly);
214
- assert.ok(calendarOnly);
215
- assert.deepEqual(
216
- mailOnly.filter((scope) => scope.includes("://")),
217
- MAIL_SCOPES,
218
- );
219
- assert.deepEqual(
220
- calendarOnly.filter((scope) => scope.includes("://")),
221
- CALENDAR_SCOPES,
222
- );
223
- assert.ok(calendarOnly.includes("offline_access"));
224
- });
225
-
226
- it("creates a calendar-only account holding the calendar grant", async () => {
227
- const sub = randomUUID();
228
-
229
- const location = await consent(sub, [AccountService.Calendar]);
230
-
231
- const account = await onlyAccount(sub);
232
- assert.equal(
233
- location,
234
- `${WEB_ORIGIN}/settings/accounts?connected=${encodeURIComponent(account.accountId)}`,
235
- );
236
- assert.deepEqual(account.syncedServices, [AccountService.Calendar]);
237
- assert.deepEqual(account.grantedScopes, CALENDAR_SCOPES);
238
- assert.deepEqual(redemptions, [
239
- [...CALENDAR_SCOPES, "offline_access", "openid", "email"],
240
- ]);
241
- });
242
-
243
- it("refuses a consent that comes back without the calendar scope and writes nothing", async () => {
244
- const sub = randomUUID();
245
- withheld = new Set(CALENDAR_SCOPES);
246
-
247
- const location = await consent(sub, [
248
- AccountService.Mail,
249
- AccountService.Calendar,
250
- ]);
251
-
252
- assert.equal(
253
- location,
254
- `${WEB_ORIGIN}/settings/accounts?oauthError=scope_not_granted&oauthEmail=person%40example.com&missingServices=Calendar`,
255
- );
256
- assert.deepEqual(
257
- await client.account.listAllByAccountConfig(deriveAccountConfigId(sub)),
258
- [],
259
- );
260
- });
261
-
262
- it("keeps calendar when a mail and calendar account reconnects naming only its email", async () => {
263
- const sub = randomUUID();
264
- await consent(sub, [AccountService.Mail, AccountService.Calendar]);
265
- const connected = await onlyAccount(sub);
266
-
267
- redemptions = [];
268
- const location = await consent(sub, undefined);
269
-
270
- const reconnected = await onlyAccount(sub);
271
- assert.equal(
272
- location,
273
- `${WEB_ORIGIN}/settings/accounts?connected=${encodeURIComponent(connected.accountId)}`,
274
- );
275
- assert.deepEqual(reconnected.syncedServices, [
276
- AccountService.Mail,
277
- AccountService.Calendar,
278
- ]);
279
- assert.deepEqual(reconnected.grantedScopes, [
280
- ...MAIL_SCOPES,
281
- ...CALENDAR_SCOPES,
282
- ]);
283
- assert.deepEqual(
284
- redemptions.map((scopes) => scopes.filter((s) => s.includes("://"))),
285
- [MAIL_SCOPES, CALENDAR_SCOPES],
286
- );
287
- });
288
-
289
- it("adds calendar to a mail account through a second consent", async () => {
290
- const sub = randomUUID();
291
- await consent(sub, [AccountService.Mail]);
292
- const mailAccount = await onlyAccount(sub);
293
- assert.deepEqual(mailAccount.grantedScopes, MAIL_SCOPES);
294
-
295
- const refused = await patchServices(sub, mailAccount.accountId, [
296
- AccountService.Mail,
297
- AccountService.Calendar,
298
- ]);
299
- assert.equal(refused.statusCode, 400, refused.body);
300
- const { message } = JSON.parse(refused.body) as { message: string };
301
- assert.match(message, /granted scopes lack Calendar/);
302
- assert.match(message, /POST \/accounts\/oauth\/microsoft\/start/);
303
-
304
- redemptions = [];
305
- await consent(sub, [AccountService.Mail, AccountService.Calendar]);
306
-
307
- const reconnected = await onlyAccount(sub);
308
- assert.equal(reconnected.accountId, mailAccount.accountId);
309
- assert.deepEqual(reconnected.syncedServices, [
310
- AccountService.Mail,
311
- AccountService.Calendar,
312
- ]);
313
- assert.deepEqual(reconnected.grantedScopes, [
314
- ...MAIL_SCOPES,
315
- ...CALENDAR_SCOPES,
316
- ]);
317
- assert.deepEqual(
318
- redemptions.map((scopes) => scopes.filter((s) => s.includes("://"))),
319
- [MAIL_SCOPES, CALENDAR_SCOPES],
320
- );
321
-
322
- const mailAgain = await patchServices(sub, mailAccount.accountId, [
323
- AccountService.Mail,
324
- ]);
325
- assert.equal(mailAgain.statusCode, 200, mailAgain.body);
326
- const both = await patchServices(sub, mailAccount.accountId, [
327
- AccountService.Calendar,
328
- AccountService.Mail,
329
- ]);
330
- assert.equal(both.statusCode, 200, both.body);
331
- assert.deepEqual(
332
- (JSON.parse(both.body) as AccountResponse).syncedServices,
333
- [AccountService.Mail, AccountService.Calendar],
334
- );
335
- });
336
- });
@@ -1,191 +0,0 @@
1
- /**
2
- * What an operator can tell from the log when the Microsoft OAuth start fails.
3
- *
4
- * Every credential failure answers the same 500 with the same body, so the
5
- * backend log for that request is the only place the four are told apart: an
6
- * instance with no Microsoft app registered must not read like a live outage.
7
- * Driven through the real handler so what is asserted is the line a deployment
8
- * actually writes, correlation id and all.
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import { randomUUID } from "node:crypto";
13
- import { after, before, describe, it } from "node:test";
14
- import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
15
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
16
- import { captureStdout, restoreStdout } from "../stdout-capture-fixture.js";
17
-
18
- const WEB_ORIGIN = "https://mail.example.com";
19
- const START_PATH = "/accounts/oauth/microsoft/start";
20
- const SECRET_ARN =
21
- "arn:aws:secretsmanager:eu-west-1:000000000000:secret:msoauth-AbCdEf";
22
-
23
- let handler: (
24
- event: APIGatewayProxyEvent,
25
- context: unknown,
26
- ) => Promise<APIGatewayProxyResult>;
27
-
28
- const originalSend = SecretsManagerClient.prototype.send;
29
- let secretString: string | undefined;
30
-
31
- interface Outcome {
32
- response: APIGatewayProxyResult;
33
- log: string;
34
- logged: string;
35
- }
36
-
37
- const headerOf = (
38
- response: APIGatewayProxyResult,
39
- name: string,
40
- ): string | undefined => {
41
- for (const [key, value] of Object.entries(response.headers ?? {})) {
42
- if (key.toLowerCase() === name) return String(value);
43
- }
44
- return undefined;
45
- };
46
-
47
- const start = async (): Promise<Outcome> => {
48
- const awsRequestId = `req-${randomUUID()}`;
49
- const { result: response, logged } = await captureStdout(() =>
50
- handler(
51
- {
52
- httpMethod: "POST",
53
- path: START_PATH,
54
- headers: {},
55
- queryStringParameters: null,
56
- body: JSON.stringify({ email: "matthijs@example.com" }),
57
- requestContext: { authorizer: { claims: { sub: "cognito-sub" } } },
58
- } as unknown as APIGatewayProxyEvent,
59
- { awsRequestId, functionName: "test" },
60
- ),
61
- );
62
-
63
- assert.equal(
64
- headerOf(response, "x-correlation-id"),
65
- awsRequestId,
66
- "the id the client reports has to be the id the log lines carry",
67
- );
68
-
69
- const forThisRequest = logged
70
- .split("\n")
71
- .filter((line) => line.length > 0)
72
- .map((line) => JSON.parse(line) as Record<string, unknown>)
73
- .filter((line) => line.requestId === awsRequestId);
74
-
75
- assert.ok(
76
- forThisRequest.length > 0,
77
- "the failure left no line carrying the correlation id the client was handed",
78
- );
79
-
80
- return { response, log: JSON.stringify(forThisRequest), logged };
81
- };
82
-
83
- /** The name each condition, and only that condition, puts in the log. */
84
- const NO_APP_REGISTERED = "MicrosoftOAuthNotConfigured";
85
- const SECRET_HAS_NO_STRING = "MicrosoftOAuthSecretEmpty";
86
- const SECRET_MISSING_FIELDS = "MicrosoftOAuthSecretIncomplete";
87
- const SECRET_IS_NOT_JSON = "MicrosoftOAuthSecretUnparseable";
88
-
89
- const CONDITIONS = [
90
- NO_APP_REGISTERED,
91
- SECRET_HAS_NO_STRING,
92
- SECRET_MISSING_FIELDS,
93
- SECRET_IS_NOT_JSON,
94
- ];
95
-
96
- const assertNamesOnly = (log: string, condition: string): void => {
97
- assert.ok(
98
- log.includes(condition),
99
- `the log for this request never names the condition ${condition}: ${log}`,
100
- );
101
- for (const other of CONDITIONS) {
102
- if (other === condition) continue;
103
- assert.ok(
104
- !log.includes(other),
105
- `the log also names ${other}, so the two conditions are indistinguishable`,
106
- );
107
- }
108
- };
109
-
110
- const assertGenericFiveHundred = (response: APIGatewayProxyResult): void => {
111
- assert.equal(response.statusCode, 500);
112
- assert.deepEqual(JSON.parse(response.body), {
113
- code: "internal_error",
114
- message: "Internal server error",
115
- });
116
- };
117
-
118
- before(async () => {
119
- process.env.LOG_LEVEL = "error";
120
- process.env.DATA_BACKEND = "sqlite";
121
- process.env.CORS_ALLOWED_ORIGINS = WEB_ORIGIN;
122
- process.env.MSOAUTH_REDIRECT_URI = `${WEB_ORIGIN}/accounts/oauth/microsoft/callback`;
123
-
124
- SecretsManagerClient.prototype.send = (async () => ({
125
- SecretString: secretString,
126
- })) as typeof SecretsManagerClient.prototype.send;
127
-
128
- ({ handler } = (await import("../index.js")) as unknown as {
129
- handler: typeof handler;
130
- });
131
- });
132
-
133
- after(() => {
134
- SecretsManagerClient.prototype.send = originalSend;
135
- restoreStdout();
136
- });
137
-
138
- describe("a Microsoft OAuth start that cannot load its credentials", () => {
139
- it("names an instance with no Microsoft app registered", async () => {
140
- delete process.env.MSOAUTH_CLIENT_ID;
141
- delete process.env.MSOAUTH_CLIENT_SECRET;
142
- delete process.env.MSOAUTH_SECRET_ARN;
143
-
144
- const { response, log } = await start();
145
-
146
- assertGenericFiveHundred(response);
147
- assertNamesOnly(log, NO_APP_REGISTERED);
148
- });
149
-
150
- it("names a secret that carries no string value", async () => {
151
- delete process.env.MSOAUTH_CLIENT_ID;
152
- delete process.env.MSOAUTH_CLIENT_SECRET;
153
- process.env.MSOAUTH_SECRET_ARN = SECRET_ARN;
154
- secretString = undefined;
155
-
156
- const { response, log } = await start();
157
-
158
- assertGenericFiveHundred(response);
159
- assertNamesOnly(log, SECRET_HAS_NO_STRING);
160
- });
161
-
162
- it("names a secret whose credential fields are absent", async () => {
163
- delete process.env.MSOAUTH_CLIENT_ID;
164
- delete process.env.MSOAUTH_CLIENT_SECRET;
165
- process.env.MSOAUTH_SECRET_ARN = SECRET_ARN;
166
- secretString = JSON.stringify({ clientId: "an-app-id" });
167
-
168
- const { response, log } = await start();
169
-
170
- assertGenericFiveHundred(response);
171
- assertNamesOnly(log, SECRET_MISSING_FIELDS);
172
- });
173
-
174
- it("names a secret that is not JSON without quoting what it holds", async () => {
175
- const secretMaterial = "s3cr3t-not-json-9d41c0b7";
176
- delete process.env.MSOAUTH_CLIENT_ID;
177
- delete process.env.MSOAUTH_CLIENT_SECRET;
178
- process.env.MSOAUTH_SECRET_ARN = SECRET_ARN;
179
- secretString = secretMaterial;
180
-
181
- const { response, log, logged } = await start();
182
-
183
- assertGenericFiveHundred(response);
184
- assertNamesOnly(log, SECRET_IS_NOT_JSON);
185
- assert.equal(
186
- logged.includes(secretMaterial),
187
- false,
188
- "a parse failure quoting the secret into the log turns a misconfiguration into a leak",
189
- );
190
- });
191
- });