@remit/imap-worker 0.0.90 → 0.0.91

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 (172) 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 +26 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +27 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
  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/{src/scheduler/handler.ts → dist/scheduler/handler.js} +25 -42
  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 +62 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +57 -78
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/sqs-queues.d.ts +4 -0
  92. package/dist/sqs-queues.js +22 -0
  93. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  94. package/dist/with-oauth-lifecycle-deps.js +17 -0
  95. package/dist/with-oauth-lifecycle.d.ts +56 -0
  96. package/dist/with-oauth-lifecycle.js +92 -0
  97. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  98. package/dist/worker-version.js +12 -0
  99. package/package.json +75 -69
  100. package/src/account-check.test.ts +0 -149
  101. package/src/account-check.ts +0 -101
  102. package/src/body-sync-gate.test.ts +0 -185
  103. package/src/body-sync-gate.ts +0 -86
  104. package/src/cli.ts +0 -218
  105. package/src/connection-scope.test.ts +0 -266
  106. package/src/connection-scope.ts +0 -314
  107. package/src/credential-less-sync-fence.test.ts +0 -152
  108. package/src/e2e-processor-shim.ts +0 -232
  109. package/src/emit.test.ts +0 -145
  110. package/src/emit.ts +0 -115
  111. package/src/events.ts +0 -252
  112. package/src/failure-kind.test.ts +0 -42
  113. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  114. package/src/handlers/append-sent-message.ts +0 -264
  115. package/src/handlers/delete-account-objects.test.ts +0 -126
  116. package/src/handlers/delete-account-objects.ts +0 -96
  117. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  118. package/src/handlers/empty-trash.ts +0 -352
  119. package/src/handlers/flag-push.ts +0 -285
  120. package/src/handlers/imported-folder.test.ts +0 -63
  121. package/src/handlers/imported-folder.ts +0 -38
  122. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  123. package/src/handlers/mailbox-management.ts +0 -486
  124. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  125. package/src/handlers/mailbox-sync-order.ts +0 -65
  126. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  127. package/src/handlers/message-copy.ts +0 -569
  128. package/src/handlers/message-delete-terminal.ts +0 -147
  129. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  130. package/src/handlers/message-delete.ts +0 -888
  131. package/src/handlers/message-move.sqlite.test.ts +0 -271
  132. package/src/handlers/message-move.test.ts +0 -216
  133. package/src/handlers/message-move.ts +0 -755
  134. package/src/handlers/placement-move-push.test.ts +0 -137
  135. package/src/handlers/placement-move-push.ts +0 -478
  136. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  137. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  138. package/src/handlers/probe-message-id.ts +0 -15
  139. package/src/handlers/sync-mailboxes.test.ts +0 -183
  140. package/src/handlers/sync-mailboxes.ts +0 -425
  141. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  142. package/src/handlers/sync-message-body.test.ts +0 -150
  143. package/src/handlers/sync-message-body.ts +0 -366
  144. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  145. package/src/handlers/sync-messages.ts +0 -537
  146. package/src/index.test.ts +0 -22
  147. package/src/index.ts +0 -82
  148. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  149. package/src/mailbox-row.ts +0 -20
  150. package/src/pending-sync.sqlite.test.ts +0 -165
  151. package/src/poller.ts +0 -55
  152. package/src/processor.ts +0 -62
  153. package/src/queues.ts +0 -40
  154. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  155. package/src/scheduler/attachment-sweep.ts +0 -46
  156. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  157. package/src/scheduler/calendar-subscriptions.ts +0 -103
  158. package/src/scheduler/config.test.ts +0 -40
  159. package/src/scheduler/decide-due.test.ts +0 -44
  160. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  161. package/src/scheduler/loop.test.ts +0 -143
  162. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  163. package/src/scheduler/run-tick.ts +0 -281
  164. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  165. package/src/stranded-placement.sqlite.test.ts +0 -799
  166. package/src/test-helpers/test-world.ts +0 -425
  167. package/src/test-helpers/worker-queues.ts +0 -11
  168. package/src/thread-join.sqlite.test.ts +0 -106
  169. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  170. package/src/with-oauth-lifecycle-deps.ts +0 -71
  171. package/src/with-oauth-lifecycle.ts +0 -158
  172. 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
- }