@remit/imap-worker 0.0.90 → 0.0.92

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 (176) hide show
  1. package/dist/account-check.d.ts +27 -0
  2. package/dist/account-check.js +70 -0
  3. package/dist/body-sync-gate.d.ts +12 -0
  4. package/dist/body-sync-gate.js +53 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +193 -0
  7. package/dist/config-env.d.ts +5 -0
  8. package/dist/config-env.js +37 -0
  9. package/dist/connection-pool.d.ts +22 -0
  10. package/dist/connection-pool.js +97 -0
  11. package/dist/connection-scope.d.ts +8 -0
  12. package/dist/connection-scope.js +10 -0
  13. package/dist/context.d.ts +46 -0
  14. package/dist/context.js +15 -0
  15. package/dist/e2e-processor-shim.d.ts +2 -0
  16. package/dist/e2e-processor-shim.js +181 -0
  17. package/dist/emit.d.ts +15 -0
  18. package/dist/emit.js +76 -0
  19. package/dist/events.d.ts +203 -0
  20. package/dist/events.js +1 -0
  21. package/dist/failure-kind.d.ts +10 -0
  22. package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
  23. package/dist/handlers/append-sent-message.d.ts +3 -0
  24. package/dist/handlers/append-sent-message.js +152 -0
  25. package/dist/handlers/delete-account-objects.d.ts +3 -0
  26. package/dist/handlers/delete-account-objects.js +35 -0
  27. package/dist/handlers/empty-trash.d.ts +14 -0
  28. package/dist/handlers/empty-trash.js +217 -0
  29. package/dist/handlers/flag-push.d.ts +38 -0
  30. package/dist/handlers/flag-push.js +195 -0
  31. package/dist/handlers/imported-folder.d.ts +4 -0
  32. package/dist/handlers/imported-folder.js +23 -0
  33. package/dist/handlers/mailbox-management.d.ts +6 -0
  34. package/dist/handlers/mailbox-management.js +294 -0
  35. package/dist/handlers/mailbox-sync-order.d.ts +21 -0
  36. package/dist/handlers/mailbox-sync-order.js +50 -0
  37. package/dist/handlers/message-copy.d.ts +40 -0
  38. package/dist/handlers/message-copy.js +371 -0
  39. package/dist/handlers/message-delete-terminal.d.ts +60 -0
  40. package/dist/handlers/message-delete-terminal.js +77 -0
  41. package/dist/handlers/message-delete.d.ts +57 -0
  42. package/dist/handlers/message-delete.js +520 -0
  43. package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
  44. package/dist/handlers/message-move-terminal.js +94 -0
  45. package/dist/handlers/message-move.d.ts +146 -0
  46. package/dist/handlers/message-move.js +490 -0
  47. package/dist/handlers/placement-move-push.d.ts +62 -0
  48. package/dist/handlers/placement-move-push.js +307 -0
  49. package/dist/handlers/probe-message-id.d.ts +2 -0
  50. package/dist/handlers/probe-message-id.js +6 -0
  51. package/dist/handlers/sync-mailboxes.d.ts +45 -0
  52. package/dist/handlers/sync-mailboxes.js +233 -0
  53. package/dist/handlers/sync-message-body.d.ts +23 -0
  54. package/dist/handlers/sync-message-body.js +182 -0
  55. package/dist/handlers/sync-messages.d.ts +29 -0
  56. package/dist/handlers/sync-messages.js +285 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +67 -0
  59. package/dist/lambda-context.d.ts +8 -0
  60. package/dist/lambda-context.js +33 -0
  61. package/dist/mailbox-row.d.ts +9 -0
  62. package/dist/mailbox-row.js +6 -0
  63. package/dist/poller.d.ts +1 -0
  64. package/dist/poller.js +51 -0
  65. package/dist/process.d.ts +6 -0
  66. package/dist/process.js +3 -0
  67. package/dist/processor.d.ts +3 -0
  68. package/dist/processor.js +44 -0
  69. package/dist/queues.d.ts +10 -0
  70. package/dist/queues.js +1 -0
  71. package/dist/s3-account-objects.d.ts +3 -0
  72. package/dist/s3-account-objects.js +26 -0
  73. package/dist/scheduler/attachment-sweep.d.ts +11 -0
  74. package/dist/scheduler/attachment-sweep.js +23 -0
  75. package/dist/scheduler/calendar-subscriptions.d.ts +22 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +29 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +9 -28
  79. package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
  80. package/dist/scheduler/decide-due.js +17 -0
  81. package/dist/scheduler/handler.d.ts +21 -0
  82. package/dist/scheduler/handler.js +50 -0
  83. package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
  84. package/dist/scheduler/loop.js +8 -0
  85. package/dist/scheduler/run-tick.d.ts +66 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/dist/scheduler/runner.js +102 -0
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/scheduler/tick.d.ts +5 -0
  92. package/dist/scheduler/tick.js +5 -0
  93. package/dist/sqs-queues.d.ts +4 -0
  94. package/dist/sqs-queues.js +22 -0
  95. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  96. package/dist/with-oauth-lifecycle-deps.js +17 -0
  97. package/dist/with-oauth-lifecycle.d.ts +56 -0
  98. package/dist/with-oauth-lifecycle.js +92 -0
  99. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  100. package/dist/worker-version.js +12 -0
  101. package/package.json +79 -69
  102. package/src/account-check.test.ts +0 -149
  103. package/src/account-check.ts +0 -101
  104. package/src/body-sync-gate.test.ts +0 -185
  105. package/src/body-sync-gate.ts +0 -86
  106. package/src/cli.ts +0 -218
  107. package/src/connection-scope.test.ts +0 -266
  108. package/src/connection-scope.ts +0 -314
  109. package/src/credential-less-sync-fence.test.ts +0 -152
  110. package/src/e2e-processor-shim.ts +0 -232
  111. package/src/emit.test.ts +0 -145
  112. package/src/emit.ts +0 -115
  113. package/src/events.ts +0 -252
  114. package/src/failure-kind.test.ts +0 -42
  115. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  116. package/src/handlers/append-sent-message.ts +0 -264
  117. package/src/handlers/delete-account-objects.test.ts +0 -126
  118. package/src/handlers/delete-account-objects.ts +0 -96
  119. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  120. package/src/handlers/empty-trash.ts +0 -352
  121. package/src/handlers/flag-push.ts +0 -285
  122. package/src/handlers/imported-folder.test.ts +0 -63
  123. package/src/handlers/imported-folder.ts +0 -38
  124. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  125. package/src/handlers/mailbox-management.ts +0 -486
  126. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  127. package/src/handlers/mailbox-sync-order.ts +0 -65
  128. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  129. package/src/handlers/message-copy.ts +0 -569
  130. package/src/handlers/message-delete-terminal.ts +0 -147
  131. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  132. package/src/handlers/message-delete.ts +0 -888
  133. package/src/handlers/message-move.sqlite.test.ts +0 -271
  134. package/src/handlers/message-move.test.ts +0 -216
  135. package/src/handlers/message-move.ts +0 -755
  136. package/src/handlers/placement-move-push.test.ts +0 -137
  137. package/src/handlers/placement-move-push.ts +0 -478
  138. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  139. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  140. package/src/handlers/probe-message-id.ts +0 -15
  141. package/src/handlers/sync-mailboxes.test.ts +0 -183
  142. package/src/handlers/sync-mailboxes.ts +0 -425
  143. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  144. package/src/handlers/sync-message-body.test.ts +0 -150
  145. package/src/handlers/sync-message-body.ts +0 -366
  146. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  147. package/src/handlers/sync-messages.ts +0 -537
  148. package/src/index.test.ts +0 -22
  149. package/src/index.ts +0 -82
  150. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  151. package/src/mailbox-row.ts +0 -20
  152. package/src/pending-sync.sqlite.test.ts +0 -165
  153. package/src/poller.ts +0 -55
  154. package/src/processor.ts +0 -62
  155. package/src/queues.ts +0 -40
  156. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  157. package/src/scheduler/attachment-sweep.ts +0 -46
  158. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  159. package/src/scheduler/calendar-subscriptions.ts +0 -103
  160. package/src/scheduler/config.test.ts +0 -40
  161. package/src/scheduler/decide-due.test.ts +0 -44
  162. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  163. package/src/scheduler/handler.ts +0 -69
  164. package/src/scheduler/loop.test.ts +0 -143
  165. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  166. package/src/scheduler/run-tick.ts +0 -281
  167. package/src/scheduler/runner.ts +0 -125
  168. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  169. package/src/stranded-placement.sqlite.test.ts +0 -799
  170. package/src/test-helpers/test-world.ts +0 -425
  171. package/src/test-helpers/worker-queues.ts +0 -11
  172. package/src/thread-join.sqlite.test.ts +0 -106
  173. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  174. package/src/with-oauth-lifecycle-deps.ts +0 -71
  175. package/src/with-oauth-lifecycle.ts +0 -158
  176. package/tsconfig.json +0 -8
@@ -1,269 +0,0 @@
1
- /**
2
- * What the account row holds after the lifecycle has written to it.
3
- *
4
- * The deps builder is the half that talks to the store, so it is tested against
5
- * the real repository over the shipped SQLite schema rather than a stand-in
6
- * that would only agree with itself: whether a stored reason survives, and
7
- * whether one that no longer applies is gone, is decided by the repository's
8
- * update semantics, where an absent field means "leave it alone".
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import { after, before, describe, it } from "node:test";
13
- import type { AccountItem } from "@remit/data-ports";
14
- import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
15
- import {
16
- type AccountRepo,
17
- createSqliteRepositories,
18
- } from "@remit/drizzle-service";
19
- import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
20
- import { noopLogger } from "@remit/logger";
21
- import { RefreshTokenError } from "@remit/mail-oauth-service";
22
- import { MailConnectionError } from "@remit/mailbox-service/types";
23
- import type { SecretsService } from "@remit/secrets-service";
24
- import {
25
- type OAuthLifecycleDeps,
26
- withOAuthLifecycle,
27
- } from "./with-oauth-lifecycle.js";
28
- import { buildLifecycleDeps } from "./with-oauth-lifecycle-deps.js";
29
-
30
- const secrets = {
31
- decrypt: async () => "decrypted",
32
- encrypt: async () => ({}) as never,
33
- } as unknown as SecretsService;
34
-
35
- /**
36
- * The real deps, with only credential resolution stubbed — minting a token is
37
- * a call to Microsoft and not what these tests are about. `updateConnectionState`
38
- * is the one under test and stays exactly as the handlers build it.
39
- */
40
- const deps = (): OAuthLifecycleDeps => ({
41
- ...buildLifecycleDeps(secrets, repo),
42
- resolveCredentials: async () => ({
43
- status: "resolved",
44
- credentials: { kind: "accessToken", accessToken: "access-token" },
45
- }),
46
- });
47
-
48
- const STALE_SYNC_ERROR = "Mailbox does not exist: [NONEXISTENT]";
49
- const REFUSAL =
50
- "IMAP authentication failed: LOGIN failed. User is authenticated but not connected.";
51
-
52
- let repo: AccountRepo;
53
- let close: () => void;
54
-
55
- const account = async (): Promise<AccountItem> =>
56
- repo.create({
57
- accountConfigId: "cfg-1",
58
- username: "matthijs@example.com",
59
- email: "matthijs@example.com",
60
- authType: AccountAuthType.OauthMicrosoft,
61
- oauthRefreshTokenHash: "encrypted-refresh-token",
62
- imapHost: "outlook.office365.com",
63
- imapPort: 993,
64
- imapTls: true,
65
- imapStartTls: false,
66
- isActive: true,
67
- connectionState: ConnectionState.Authenticated,
68
- });
69
-
70
- before(() => {
71
- const db = createShippedSqliteDb();
72
- close = db.close;
73
- repo = createSqliteRepositories({
74
- app: db.db,
75
- mail: db.db,
76
- search: db.db,
77
- }).account;
78
- });
79
-
80
- after(() => {
81
- close();
82
- });
83
-
84
- describe("the reason an account card reads back", () => {
85
- it("keeps the refusal the mail server gave", async () => {
86
- const created = await account();
87
-
88
- await withOAuthLifecycle(deps(), created, noopLogger, async () => {
89
- throw new MailConnectionError("auth", REFUSAL);
90
- });
91
-
92
- const stored = await repo.get(created.accountId);
93
- assert.equal(stored.connectionState, ConnectionState.ReauthRequired);
94
- assert.equal(stored.lastError, REFUSAL);
95
- });
96
-
97
- it("drops a reason the account has moved on from", async () => {
98
- const created = await account();
99
- // What a failed sync leaves behind (sync-mailboxes.ts): a folder that went
100
- // away, nothing to do with signing in.
101
- await repo.update(created.accountId, { lastError: STALE_SYNC_ERROR });
102
-
103
- // Hours later the refresh token is revoked. Re-authenticating is now the
104
- // whole story, and the mailbox that went missing is not it.
105
- await withOAuthLifecycle(
106
- deps(),
107
- await repo.get(created.accountId),
108
- noopLogger,
109
- async () => {
110
- throw new RefreshTokenError({
111
- kind: "reauth-required",
112
- code: "invalid_grant",
113
- });
114
- },
115
- );
116
-
117
- const stored = await repo.get(created.accountId);
118
- assert.equal(stored.connectionState, ConnectionState.ReauthRequired);
119
- assert.equal(
120
- stored.lastError,
121
- undefined,
122
- "a stale sync error beside a Reconnect button reads as the reason for it",
123
- );
124
- });
125
-
126
- it("stores a chatty refusal at a length a card can hold", async () => {
127
- const created = await account();
128
-
129
- await withOAuthLifecycle(deps(), created, noopLogger, async () => {
130
- throw new MailConnectionError("auth", "x".repeat(4000));
131
- });
132
-
133
- const stored = await repo.get(created.accountId);
134
- assert.equal(stored.lastError?.length, 500);
135
- });
136
- });
137
-
138
- describe("which failures stop the work, and what the account row says after", () => {
139
- const passwordAccount = (passwordHash?: string): Promise<AccountItem> =>
140
- repo.create({
141
- accountConfigId: "cfg-1",
142
- username: "person@example.com",
143
- email: "person@example.com",
144
- authType: AccountAuthType.Password,
145
- passwordHash,
146
- imapHost: "imap.example.com",
147
- imapPort: 993,
148
- imapTls: true,
149
- imapStartTls: false,
150
- isActive: true,
151
- connectionState: ConnectionState.Authenticated,
152
- });
153
-
154
- it("runs nothing for an account that has to sign in again", async () => {
155
- const created = await account();
156
- await repo.update(created.accountId, {
157
- connectionState: ConnectionState.ReauthRequired,
158
- });
159
- let worked = false;
160
-
161
- await withOAuthLifecycle(
162
- deps(),
163
- await repo.get(created.accountId),
164
- noopLogger,
165
- async () => {
166
- worked = true;
167
- },
168
- );
169
-
170
- assert.equal(worked, false);
171
- });
172
-
173
- // Issue #1120: a password account with nothing stored can only fail, and a
174
- // failing batch item fences the account's FIFO group.
175
- it("acks a password account that stores no password and says the credential is missing", async () => {
176
- const created = await passwordAccount();
177
- let worked = false;
178
-
179
- await withOAuthLifecycle(
180
- buildLifecycleDeps(secrets, repo),
181
- created,
182
- noopLogger,
183
- async () => {
184
- worked = true;
185
- },
186
- );
187
-
188
- assert.equal(worked, false);
189
- assert.equal(
190
- (await repo.get(created.accountId)).connectionState,
191
- ConnectionState.CredentialsMissing,
192
- );
193
- });
194
-
195
- it("acks an OAuth account that holds no refresh token and asks for a sign-in", async () => {
196
- const created = await repo.create({
197
- accountConfigId: "cfg-1",
198
- username: "matthijs@example.com",
199
- email: "matthijs@example.com",
200
- authType: AccountAuthType.OauthMicrosoft,
201
- imapHost: "outlook.office365.com",
202
- imapPort: 993,
203
- imapTls: true,
204
- imapStartTls: false,
205
- isActive: true,
206
- connectionState: ConnectionState.Authenticated,
207
- });
208
-
209
- await withOAuthLifecycle(
210
- buildLifecycleDeps(secrets, repo),
211
- created,
212
- noopLogger,
213
- async () => {},
214
- );
215
-
216
- assert.equal(
217
- (await repo.get(created.accountId)).connectionState,
218
- ConnectionState.ReauthRequired,
219
- );
220
- });
221
-
222
- // A password that stopped working is the user's to fix in settings; the
223
- // state flip is reserved for OAuth, where a sign-in is the fix.
224
- for (const [name, error] of [
225
- ["a refused password", new MailConnectionError("auth", "auth failed")],
226
- ["a network failure", new MailConnectionError("network", "timeout")],
227
- ["any other error", new Error("boom")],
228
- ] as const) {
229
- it(`redelivers ${name} on a password account and leaves its state alone`, async () => {
230
- const created = await passwordAccount('{"ciphertext":"x"}');
231
-
232
- await assert.rejects(
233
- withOAuthLifecycle(deps(), created, noopLogger, async () => {
234
- throw error;
235
- }),
236
- error,
237
- );
238
-
239
- assert.equal(
240
- (await repo.get(created.accountId)).connectionState,
241
- ConnectionState.Authenticated,
242
- );
243
- });
244
- }
245
-
246
- it("redelivers a transient token failure and leaves the state alone", async () => {
247
- const created = await account();
248
-
249
- await assert.rejects(
250
- withOAuthLifecycle(
251
- {
252
- ...deps(),
253
- resolveCredentials: async () => {
254
- throw new RefreshTokenError({ kind: "transient", code: "503" });
255
- },
256
- },
257
- created,
258
- noopLogger,
259
- async () => {},
260
- ),
261
- /transient/,
262
- );
263
-
264
- assert.equal(
265
- (await repo.get(created.accountId)).connectionState,
266
- ConnectionState.Authenticated,
267
- );
268
- });
269
- });
@@ -1,71 +0,0 @@
1
- /**
2
- * with-oauth-lifecycle-deps.ts
3
- *
4
- * Shared construction of the deps passed to withOAuthLifecycle by every IMAP
5
- * handler. Centralizes the lazy OAuth token service, secrets, refresh-token
6
- * rotation persistence, and the connectionState writer so handlers don't each
7
- * re-declare the same boilerplate.
8
- */
9
-
10
- import type { IAccountRepository } from "@remit/data-ports";
11
- import {
12
- createMailOAuthService,
13
- microsoftProviderConfig,
14
- } from "@remit/mail-oauth-service";
15
- import type { SecretsService } from "@remit/secrets-service";
16
- import type {
17
- ConnectionStateValue,
18
- OAuthLifecycleDeps,
19
- } from "./with-oauth-lifecycle.js";
20
-
21
- // Lazy OAuth service: created on first OAuth account.
22
- // Uses MSOAUTH_* env vars which are only present in deployed Lambdas.
23
- let _tokenService: ReturnType<typeof createMailOAuthService> | undefined;
24
- const getTokenService = (): ReturnType<typeof createMailOAuthService> => {
25
- if (!_tokenService) {
26
- _tokenService = createMailOAuthService(
27
- microsoftProviderConfig({
28
- clientId: process.env.MSOAUTH_CLIENT_ID ?? "",
29
- clientSecret: process.env.MSOAUTH_CLIENT_SECRET ?? "",
30
- overrides: process.env.MSOAUTH_TOKEN_ENDPOINT
31
- ? { tokenEndpoint: process.env.MSOAUTH_TOKEN_ENDPOINT }
32
- : undefined,
33
- }),
34
- );
35
- }
36
- return _tokenService;
37
- };
38
-
39
- /**
40
- * Build the OAuthLifecycleDeps for a handler. `secrets` and `accountService`
41
- * are the long-lived singletons already constructed at module scope in each
42
- * handler.
43
- */
44
- export const buildLifecycleDeps = (
45
- secrets: SecretsService,
46
- accountService: IAccountRepository,
47
- ): OAuthLifecycleDeps => ({
48
- secrets,
49
- tokenService: getTokenService(),
50
- persistRotatedToken: async (accountId, encryptedHash, updatedAt) => {
51
- await accountService.update(accountId, {
52
- oauthRefreshTokenHash: encryptedHash,
53
- oauthTokenUpdatedAt: updatedAt,
54
- });
55
- },
56
- // A state flip with no reason of its own removes the stored one. Left in
57
- // place it would describe a failure the account has moved on from — the
58
- // mailbox a sync could not find, still on the card beside a Reconnect
59
- // button, after the token was revoked hours later.
60
- updateConnectionState: async (
61
- accountId: string,
62
- state: ConnectionStateValue,
63
- lastError?: string,
64
- ) => {
65
- await accountService.update(
66
- accountId,
67
- { connectionState: state, lastError },
68
- lastError === undefined ? ["lastError"] : undefined,
69
- );
70
- },
71
- });
@@ -1,158 +0,0 @@
1
- /**
2
- * with-oauth-lifecycle.ts
3
- *
4
- * Centralizes the OAuth reauth/ACK contract shared by every IMAP handler.
5
- *
6
- * The contract (see issue #472):
7
- * - If the account already requires reauth, skip all IMAP traffic entirely.
8
- * - Credential resolution AND the per-handler work both run inside one
9
- * try/catch so that a revoked OAuth token is caught regardless of where it
10
- * surfaces (token mint vs. first IMAP command).
11
- * - An account storing no credential at all is terminal, not transient: the
12
- * account is flipped to the state that describes it and the event is ACKed.
13
- * Retrying instead fenced the account's FIFO group for the message's whole
14
- * visibility timeout, blocking the sync that follows the credential the
15
- * user was still typing (issue #1120).
16
- * - On a terminal auth failure (RefreshTokenError reauth-required, or
17
- * MailConnectionError auth), flip the account to reauth_required and return
18
- * WITHOUT rethrowing — this ACKs the SQS message so it is not retried.
19
- * An IMAP refusal is stored with it, because not every refusal a mail server
20
- * hands back is one re-authenticating clears: "SmtpClientAuthentication is
21
- * disabled" needs a tenant setting changed, and an account card that only
22
- * ever says "Re-authentication required" sends the user around a loop.
23
- * - On transient / config / network errors, rethrow so SQS retries with
24
- * backoff (let-it-crash).
25
- *
26
- * Tokens must NEVER appear in logs — only accountId / errorKind / errorCode.
27
- */
28
-
29
- import type { AccountItem } from "@remit/data-ports";
30
- import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
31
- import type { Logger } from "@remit/logger";
32
- import { RefreshTokenError } from "@remit/mail-oauth-service";
33
- import {
34
- type AccountCredentialsDeps,
35
- type ConnectionStateValue,
36
- type CredentialResolution,
37
- resolveConnectionCredentials,
38
- } from "@remit/mailbox-service/account-credentials";
39
- import {
40
- MailConnectionError,
41
- type MailCredentials,
42
- } from "@remit/mailbox-service/types";
43
- import { isAccountReauthRequired } from "./account-check.js";
44
-
45
- export type { ConnectionStateValue };
46
-
47
- export interface OAuthLifecycleDeps extends AccountCredentialsDeps {
48
- /**
49
- * Persist the account's connectionState, and with it the sentence the
50
- * account card shows for the failure. `lastError` carries the server's own
51
- * words when the failure came with any; it is absent when nothing was said,
52
- * as for a credential that was never stored or a token simply revoked, and
53
- * the stored reason is then cleared rather than left to describe an older
54
- * failure the account has moved on from.
55
- */
56
- updateConnectionState: (
57
- accountId: string,
58
- state: ConnectionStateValue,
59
- lastError?: string,
60
- ) => Promise<void>;
61
- /**
62
- * Resolve credentials for the account. Defaults to
63
- * resolveConnectionCredentials; overridable for testing. Kept as the ONLY
64
- * authType branch in the codebase (see account-credentials.ts).
65
- */
66
- resolveCredentials?: (
67
- account: AccountItem,
68
- deps: AccountCredentialsDeps,
69
- ) => Promise<CredentialResolution>;
70
- }
71
-
72
- /**
73
- * Run `work` for an account under the shared OAuth reauth/ACK contract.
74
- *
75
- * `work` receives the resolved MailCredentials. Both credential resolution and
76
- * `work` run inside the same try/catch.
77
- */
78
- export const withOAuthLifecycle = async (
79
- deps: OAuthLifecycleDeps,
80
- account: AccountItem,
81
- log: Logger,
82
- work: (credentials: MailCredentials) => Promise<void>,
83
- ): Promise<void> => {
84
- // Skip all IMAP traffic for accounts that already require reauth.
85
- if (isAccountReauthRequired(account, log)) {
86
- return;
87
- }
88
-
89
- const resolve = deps.resolveCredentials ?? resolveConnectionCredentials;
90
-
91
- try {
92
- const resolution = await resolve(account, deps);
93
- if (resolution.status === "missing") {
94
- log.warn(
95
- {
96
- accountId: account.accountId,
97
- reason: resolution.reason,
98
- connectionState: resolution.terminalState,
99
- },
100
- "Account stores no credential; acking the event without IMAP traffic",
101
- );
102
- await deps.updateConnectionState(
103
- account.accountId,
104
- resolution.terminalState,
105
- );
106
- return; // ACK — no retry can produce a credential
107
- }
108
- await work(resolution.credentials);
109
- } catch (err) {
110
- // Terminal OAuth failure: token revoked / consent withdrawn.
111
- if (err instanceof RefreshTokenError) {
112
- if (err.error.kind === "reauth-required") {
113
- log.warn(
114
- {
115
- accountId: account.accountId,
116
- errorKind: err.error.kind,
117
- errorCode: err.error.code,
118
- },
119
- "OAuth token revoked; marking account reauth_required",
120
- );
121
- await deps.updateConnectionState(
122
- account.accountId,
123
- ConnectionState.ReauthRequired,
124
- );
125
- return; // ACK — do not retry
126
- }
127
- // transient or config: let-it-crash (SQS retry / DLQ)
128
- throw err;
129
- }
130
-
131
- // Terminal auth failure at the IMAP layer (bad credentials / expired token).
132
- // Only OAuth accounts can recover via the re-auth flow; password accounts
133
- // have no such path so we rethrow to preserve pre-PR batch-item-failure
134
- // behaviour instead of permanently fencing the account.
135
- if (err instanceof MailConnectionError && err.kind === "auth") {
136
- if (account.authType !== AccountAuthType.OauthMicrosoft) {
137
- throw err;
138
- }
139
- log.warn(
140
- {
141
- accountId: account.accountId,
142
- errorKind: err.kind,
143
- error: err.message,
144
- },
145
- "IMAP auth rejected; marking account reauth_required",
146
- );
147
- await deps.updateConnectionState(
148
- account.accountId,
149
- ConnectionState.ReauthRequired,
150
- err.message,
151
- );
152
- return; // ACK — do not retry
153
- }
154
-
155
- // Transient / network / unexpected: rethrow so SQS retries with backoff.
156
- throw err;
157
- }
158
- };
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src/**/*.ts"]
8
- }