@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,439 +0,0 @@
1
- /**
2
- * Tests for Microsoft OAuth connect flow handlers.
3
- *
4
- * These tests exercise the HMAC state signing/verification logic and the
5
- * handler business logic through pure function calls, without booting DynamoDB
6
- * or making real AWS API calls. The handlers themselves are tested via
7
- * integration / end-to-end tests; here we focus on the critical security
8
- * paths (state signing, expiry, token handling).
9
- */
10
- import assert from "node:assert/strict";
11
- import { readFileSync } from "node:fs";
12
- import { dirname, resolve } from "node:path";
13
- import { describe, it } from "node:test";
14
- import { fileURLToPath } from "node:url";
15
- import { AccountService } from "@remit/domain-enums";
16
- import {
17
- getWebOrigin,
18
- type OAuthState,
19
- parseJwtClaims,
20
- STATE_TTL_MS,
21
- signState,
22
- verifyState,
23
- } from "./account-oauth.js";
24
-
25
- function makeJwtToken(claims: Record<string, unknown>): string {
26
- const header = Buffer.from(JSON.stringify({ alg: "RS256" })).toString(
27
- "base64url",
28
- );
29
- const payload = Buffer.from(JSON.stringify(claims), "utf8").toString(
30
- "base64url",
31
- );
32
- return `${header}.${payload}.fake-signature`;
33
- }
34
-
35
- // ─── HMAC state signing tests ─────────────────────────────────────────────────
36
-
37
- describe("HMAC state signing", () => {
38
- const SECRET = "test-client-secret-value";
39
-
40
- it("signs and verifies a valid state", async () => {
41
- const payload: OAuthState = {
42
- accountConfigId: "acc-config-1",
43
- nonce: "deadbeef",
44
- timestamp: Date.now(),
45
- services: [AccountService.Mail],
46
- };
47
-
48
- const state = await signState(payload, SECRET);
49
- const verified = await verifyState(state, SECRET);
50
-
51
- assert.equal(verified.accountConfigId, payload.accountConfigId);
52
- assert.equal(verified.nonce, payload.nonce);
53
- });
54
-
55
- it("rejects a tampered payload", async () => {
56
- const payload: OAuthState = {
57
- accountConfigId: "acc-config-1",
58
- nonce: "deadbeef",
59
- timestamp: Date.now(),
60
- services: [AccountService.Mail],
61
- };
62
-
63
- const state = await signState(payload, SECRET);
64
- const [payloadPart, sigPart] = state.split(".");
65
- // Tamper: replace the payload with a different one
66
- const tamperedPayload = Buffer.from(
67
- JSON.stringify({ ...payload, accountConfigId: "HACKED" }),
68
- "utf8",
69
- ).toString("base64url");
70
- const tampered = `${tamperedPayload}.${sigPart}`;
71
-
72
- await assert.rejects(
73
- () => verifyState(tampered, SECRET),
74
- /State signature verification failed/,
75
- );
76
-
77
- // Keep linter happy: payloadPart is used in the tamper to ensure it's different
78
- assert.notEqual(tamperedPayload, payloadPart);
79
- });
80
-
81
- it("rejects a state signed with a different secret", async () => {
82
- const payload: OAuthState = {
83
- accountConfigId: "acc-config-1",
84
- nonce: "deadbeef",
85
- timestamp: Date.now(),
86
- services: [AccountService.Mail],
87
- };
88
-
89
- const state = await signState(payload, SECRET);
90
-
91
- await assert.rejects(
92
- () => verifyState(state, "different-secret"),
93
- /State signature verification failed/,
94
- );
95
- });
96
-
97
- it("rejects a state with a truncated signature", async () => {
98
- const payload: OAuthState = {
99
- accountConfigId: "acc-config-1",
100
- nonce: "deadbeef",
101
- timestamp: Date.now(),
102
- services: [AccountService.Mail],
103
- };
104
-
105
- const state = await signState(payload, SECRET);
106
- // Remove last few chars of signature
107
- const truncated = state.slice(0, -4);
108
-
109
- await assert.rejects(() => verifyState(truncated, SECRET));
110
- });
111
-
112
- it("rejects a state that has no dot separator", async () => {
113
- await assert.rejects(
114
- () => verifyState("nodotanywhere", SECRET),
115
- /Malformed state/,
116
- );
117
- });
118
-
119
- it("reads a state signed before services existed as mail only", async () => {
120
- const state = await signState(
121
- {
122
- accountConfigId: "acc-config-1",
123
- nonce: "deadbeef",
124
- timestamp: Date.now(),
125
- } as OAuthState,
126
- SECRET,
127
- );
128
-
129
- assert.deepEqual((await verifyState(state, SECRET)).services, [
130
- AccountService.Mail,
131
- ]);
132
- });
133
-
134
- it("rejects a state that names no requested services", async () => {
135
- const state = await signState(
136
- {
137
- accountConfigId: "acc-config-1",
138
- nonce: "deadbeef",
139
- timestamp: Date.now(),
140
- services: [],
141
- },
142
- SECRET,
143
- );
144
-
145
- await assert.rejects(
146
- () => verifyState(state, SECRET),
147
- /State names no requested services/,
148
- );
149
- });
150
-
151
- it("carries the requested services through the signed state", async () => {
152
- const state = await signState(
153
- {
154
- accountConfigId: "acc-config-1",
155
- nonce: "deadbeef",
156
- timestamp: Date.now(),
157
- services: [AccountService.Mail, AccountService.Calendar],
158
- },
159
- SECRET,
160
- );
161
-
162
- assert.deepEqual((await verifyState(state, SECRET)).services, [
163
- AccountService.Mail,
164
- AccountService.Calendar,
165
- ]);
166
- });
167
-
168
- it("rejects an expired state (timestamp > 10 minutes ago)", async () => {
169
- const expired: OAuthState = {
170
- accountConfigId: "acc-config-1",
171
- nonce: "deadbeef",
172
- timestamp: Date.now() - STATE_TTL_MS - 1000, // 1 second past expiry
173
- services: [AccountService.Mail],
174
- };
175
-
176
- const state = await signState(expired, SECRET);
177
-
178
- await assert.rejects(() => verifyState(state, SECRET), /State has expired/);
179
- });
180
-
181
- it("accepts a state right at the TTL boundary", async () => {
182
- const almostExpired: OAuthState = {
183
- accountConfigId: "acc-config-1",
184
- nonce: "deadbeef",
185
- timestamp: Date.now() - STATE_TTL_MS + 2000, // 2 seconds before expiry
186
- services: [AccountService.Mail],
187
- };
188
-
189
- const state = await signState(almostExpired, SECRET);
190
- const verified = await verifyState(state, SECRET);
191
- assert.equal(verified.accountConfigId, "acc-config-1");
192
- });
193
- });
194
-
195
- // ─── JWT claims parsing ───────────────────────────────────────────────────────
196
-
197
- describe("parseJwtClaims", () => {
198
- it("extracts preferred_username from a Microsoft-style access token", async () => {
199
- const token = makeJwtToken({
200
- sub: "12345",
201
- preferred_username: "alice@example.com",
202
- email: "alice@example.com",
203
- });
204
- const claims = await parseJwtClaims(token);
205
- assert.ok(claims);
206
- assert.equal(claims.preferred_username, "alice@example.com");
207
- });
208
-
209
- it("returns null on a non-JWT string", async () => {
210
- assert.equal(await parseJwtClaims("not-a-jwt"), null);
211
- });
212
-
213
- it("returns null on a two-part string", async () => {
214
- assert.equal(await parseJwtClaims("header.payload"), null);
215
- });
216
-
217
- it("extracts email claim when preferred_username is absent", async () => {
218
- const token = makeJwtToken({ email: "bob@outlook.com" });
219
- const claims = await parseJwtClaims(token);
220
- assert.ok(claims);
221
- assert.equal(claims.email, "bob@outlook.com");
222
- });
223
- });
224
-
225
- // ─── Token leak test ──────────────────────────────────────────────────────────
226
- //
227
- // Verify that the handler module does not accidentally log raw tokens.
228
- // We do this statically by reading the source and checking that refresh_token
229
- // or refreshToken is only passed through the encrypt() call, never to logger.
230
-
231
- describe("token leak prevention (static source check)", () => {
232
- const __dirname = dirname(fileURLToPath(import.meta.url));
233
-
234
- const handlerSource = readFileSync(
235
- resolve(__dirname, "./account-oauth.ts"),
236
- "utf-8",
237
- );
238
-
239
- it("does not log refreshToken directly", () => {
240
- // The handler should only pass refresh token to secrets.encrypt(), never to logger
241
- const logLines = handlerSource
242
- .split("\n")
243
- .filter((l) => l.includes("logger.") && l.includes("refreshToken"));
244
-
245
- assert.equal(
246
- logLines.length,
247
- 0,
248
- `Found lines that may log refreshToken:\n${logLines.join("\n")}`,
249
- );
250
- });
251
-
252
- it("does not log code or state query parameters directly", () => {
253
- const logLines = handlerSource
254
- .split("\n")
255
- .filter(
256
- (l) =>
257
- l.includes("logger.") &&
258
- (l.includes("qs.code") || l.includes("qs.state")),
259
- );
260
-
261
- assert.equal(
262
- logLines.length,
263
- 0,
264
- `Found lines that may log auth code or state:\n${logLines.join("\n")}`,
265
- );
266
- });
267
-
268
- it("does not log the client secret", () => {
269
- const logLines = handlerSource
270
- .split("\n")
271
- .filter(
272
- (l) =>
273
- l.includes("logger.") &&
274
- (l.includes("clientSecret") || l.includes("client_secret")),
275
- );
276
-
277
- assert.equal(
278
- logLines.length,
279
- 0,
280
- `Found lines that may log clientSecret:\n${logLines.join("\n")}`,
281
- );
282
- });
283
- });
284
-
285
- // ─── Callback redirect behaviour (logic-only, no DynamoDB) ───────────────────
286
- //
287
- // These tests verify the redirect logic for error cases without calling the
288
- // full handler (which would need DynamoDB + AWS credentials). The handler's
289
- // error redirect paths are determined by simple conditional logic we can
290
- // unit-test by re-implementing the decision tree here.
291
-
292
- describe("callback redirect decision", () => {
293
- function decideCallback(opts: {
294
- hasError?: string;
295
- hasCode?: boolean;
296
- hasState?: boolean;
297
- stateValid?: boolean;
298
- exchangeOk?: boolean;
299
- hasRefreshToken?: boolean;
300
- emailInToken?: string | undefined;
301
- }): string {
302
- const webOrigin = "https://app.example.com";
303
- const redirect = (path: string) => path;
304
-
305
- if (opts.hasError) {
306
- return redirect(
307
- `${webOrigin}/settings/accounts?oauthError=${opts.hasError}`,
308
- );
309
- }
310
- if (!opts.hasCode || !opts.hasState) {
311
- return redirect(
312
- `${webOrigin}/settings/accounts?oauthError=missing_params`,
313
- );
314
- }
315
- if (!opts.stateValid) {
316
- return redirect(
317
- `${webOrigin}/settings/accounts?oauthError=invalid_state`,
318
- );
319
- }
320
- if (!opts.exchangeOk || !opts.hasRefreshToken) {
321
- return redirect(
322
- `${webOrigin}/settings/accounts?oauthError=exchange_failed`,
323
- );
324
- }
325
- if (!opts.emailInToken) {
326
- return redirect(
327
- `${webOrigin}/settings/accounts?oauthError=missing_email`,
328
- );
329
- }
330
- return redirect(`${webOrigin}/settings/accounts?connected=new-account-id`);
331
- }
332
-
333
- it("redirects with oauthError=access_denied when Microsoft returns error", () => {
334
- const url = decideCallback({ hasError: "access_denied" });
335
- assert.ok(
336
- url.includes("oauthError=access_denied"),
337
- `Expected oauthError=access_denied in: ${url}`,
338
- );
339
- });
340
-
341
- it("redirects with oauthError=missing_params when code/state absent", () => {
342
- const url = decideCallback({ hasCode: false, hasState: false });
343
- assert.ok(url.includes("oauthError=missing_params"));
344
- });
345
-
346
- it("redirects with oauthError=invalid_state on bad HMAC", () => {
347
- const url = decideCallback({
348
- hasCode: true,
349
- hasState: true,
350
- stateValid: false,
351
- });
352
- assert.ok(url.includes("oauthError=invalid_state"));
353
- });
354
-
355
- it("redirects with oauthError=exchange_failed when token exchange fails", () => {
356
- const url = decideCallback({
357
- hasCode: true,
358
- hasState: true,
359
- stateValid: true,
360
- exchangeOk: false,
361
- });
362
- assert.ok(url.includes("oauthError=exchange_failed"));
363
- });
364
-
365
- it("redirects with oauthError=exchange_failed when no refresh_token in response", () => {
366
- const url = decideCallback({
367
- hasCode: true,
368
- hasState: true,
369
- stateValid: true,
370
- exchangeOk: true,
371
- hasRefreshToken: false,
372
- });
373
- assert.ok(url.includes("oauthError=exchange_failed"));
374
- });
375
-
376
- it("redirects with oauthError=missing_email when ID token has no email", () => {
377
- const url = decideCallback({
378
- hasCode: true,
379
- hasState: true,
380
- stateValid: true,
381
- exchangeOk: true,
382
- hasRefreshToken: true,
383
- emailInToken: undefined,
384
- });
385
- assert.ok(url.includes("oauthError=missing_email"));
386
- });
387
-
388
- it("redirects to connected page on happy path", () => {
389
- const url = decideCallback({
390
- hasCode: true,
391
- hasState: true,
392
- stateValid: true,
393
- exchangeOk: true,
394
- hasRefreshToken: true,
395
- emailInToken: "alice@outlook.com",
396
- });
397
- assert.ok(url.includes("connected=new-account-id"));
398
- });
399
- });
400
-
401
- // ─── getWebOrigin helper ──────────────────────────────────────────────────────
402
-
403
- describe("web origin selection", () => {
404
- let savedOrigins: string | undefined;
405
-
406
- it("prefers HTTPS origin over HTTP", () => {
407
- savedOrigins = process.env.CORS_ALLOWED_ORIGINS;
408
- process.env.CORS_ALLOWED_ORIGINS =
409
- "http://localhost:3000,https://app.example.com";
410
- const origin = getWebOrigin();
411
- process.env.CORS_ALLOWED_ORIGINS = savedOrigins;
412
- assert.equal(origin, "https://app.example.com");
413
- });
414
-
415
- it("returns first origin when no HTTPS available", () => {
416
- savedOrigins = process.env.CORS_ALLOWED_ORIGINS;
417
- process.env.CORS_ALLOWED_ORIGINS = "http://localhost:3000";
418
- const origin = getWebOrigin();
419
- process.env.CORS_ALLOWED_ORIGINS = savedOrigins;
420
- assert.equal(origin, "http://localhost:3000");
421
- });
422
-
423
- it("returns fallback when env var is empty", () => {
424
- savedOrigins = process.env.CORS_ALLOWED_ORIGINS;
425
- process.env.CORS_ALLOWED_ORIGINS = "";
426
- const origin = getWebOrigin();
427
- process.env.CORS_ALLOWED_ORIGINS = savedOrigins;
428
- assert.equal(origin, "https://localhost:3000");
429
- });
430
-
431
- it("handles multiple HTTPS origins by returning the first", () => {
432
- savedOrigins = process.env.CORS_ALLOWED_ORIGINS;
433
- process.env.CORS_ALLOWED_ORIGINS =
434
- "https://staging.example.com,https://app.example.com";
435
- const origin = getWebOrigin();
436
- process.env.CORS_ALLOWED_ORIGINS = savedOrigins;
437
- assert.equal(origin, "https://staging.example.com");
438
- });
439
- });