@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.
- package/dist/account-check.d.ts +27 -0
- package/dist/account-check.js +70 -0
- package/dist/body-sync-gate.d.ts +12 -0
- package/dist/body-sync-gate.js +53 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +193 -0
- package/dist/config-env.d.ts +5 -0
- package/dist/config-env.js +37 -0
- package/dist/connection-pool.d.ts +22 -0
- package/dist/connection-pool.js +97 -0
- package/dist/connection-scope.d.ts +8 -0
- package/dist/connection-scope.js +10 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +15 -0
- package/dist/e2e-processor-shim.d.ts +2 -0
- package/dist/e2e-processor-shim.js +181 -0
- package/dist/emit.d.ts +15 -0
- package/dist/emit.js +76 -0
- package/dist/events.d.ts +203 -0
- package/dist/events.js +1 -0
- package/dist/failure-kind.d.ts +10 -0
- package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
- package/dist/handlers/append-sent-message.d.ts +3 -0
- package/dist/handlers/append-sent-message.js +152 -0
- package/dist/handlers/delete-account-objects.d.ts +3 -0
- package/dist/handlers/delete-account-objects.js +35 -0
- package/dist/handlers/empty-trash.d.ts +14 -0
- package/dist/handlers/empty-trash.js +217 -0
- package/dist/handlers/flag-push.d.ts +38 -0
- package/dist/handlers/flag-push.js +195 -0
- package/dist/handlers/imported-folder.d.ts +4 -0
- package/dist/handlers/imported-folder.js +23 -0
- package/dist/handlers/mailbox-management.d.ts +6 -0
- package/dist/handlers/mailbox-management.js +294 -0
- package/dist/handlers/mailbox-sync-order.d.ts +21 -0
- package/dist/handlers/mailbox-sync-order.js +50 -0
- package/dist/handlers/message-copy.d.ts +40 -0
- package/dist/handlers/message-copy.js +371 -0
- package/dist/handlers/message-delete-terminal.d.ts +60 -0
- package/dist/handlers/message-delete-terminal.js +77 -0
- package/dist/handlers/message-delete.d.ts +57 -0
- package/dist/handlers/message-delete.js +520 -0
- package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
- package/dist/handlers/message-move-terminal.js +94 -0
- package/dist/handlers/message-move.d.ts +146 -0
- package/dist/handlers/message-move.js +490 -0
- package/dist/handlers/placement-move-push.d.ts +62 -0
- package/dist/handlers/placement-move-push.js +307 -0
- package/dist/handlers/probe-message-id.d.ts +2 -0
- package/dist/handlers/probe-message-id.js +6 -0
- package/dist/handlers/sync-mailboxes.d.ts +45 -0
- package/dist/handlers/sync-mailboxes.js +233 -0
- package/dist/handlers/sync-message-body.d.ts +23 -0
- package/dist/handlers/sync-message-body.js +182 -0
- package/dist/handlers/sync-messages.d.ts +29 -0
- package/dist/handlers/sync-messages.js +285 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +67 -0
- package/dist/lambda-context.d.ts +8 -0
- package/dist/lambda-context.js +33 -0
- package/dist/mailbox-row.d.ts +9 -0
- package/dist/mailbox-row.js +6 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +51 -0
- package/dist/process.d.ts +6 -0
- package/dist/process.js +3 -0
- package/dist/processor.d.ts +3 -0
- package/dist/processor.js +44 -0
- package/dist/queues.d.ts +10 -0
- package/dist/queues.js +1 -0
- package/dist/s3-account-objects.d.ts +3 -0
- package/dist/s3-account-objects.js +26 -0
- package/dist/scheduler/attachment-sweep.d.ts +11 -0
- package/dist/scheduler/attachment-sweep.js +23 -0
- package/dist/scheduler/calendar-subscriptions.d.ts +22 -0
- package/dist/scheduler/calendar-subscriptions.js +35 -0
- package/dist/scheduler/config.d.ts +29 -0
- package/{src/scheduler/config.ts → dist/scheduler/config.js} +9 -28
- package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
- package/dist/scheduler/decide-due.js +17 -0
- package/dist/scheduler/handler.d.ts +21 -0
- package/dist/scheduler/handler.js +50 -0
- package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
- package/dist/scheduler/loop.js +8 -0
- package/dist/scheduler/run-tick.d.ts +66 -0
- package/dist/scheduler/run-tick.js +164 -0
- package/dist/scheduler/runner.d.ts +2 -0
- package/dist/scheduler/runner.js +102 -0
- package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
- package/dist/scheduler/stranded-placement-redrive.js +172 -0
- package/dist/scheduler/tick.d.ts +5 -0
- package/dist/scheduler/tick.js +5 -0
- package/dist/sqs-queues.d.ts +4 -0
- package/dist/sqs-queues.js +22 -0
- package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
- package/dist/with-oauth-lifecycle-deps.js +17 -0
- package/dist/with-oauth-lifecycle.d.ts +56 -0
- package/dist/with-oauth-lifecycle.js +92 -0
- package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
- package/dist/worker-version.js +12 -0
- package/package.json +79 -69
- package/src/account-check.test.ts +0 -149
- package/src/account-check.ts +0 -101
- package/src/body-sync-gate.test.ts +0 -185
- package/src/body-sync-gate.ts +0 -86
- package/src/cli.ts +0 -218
- package/src/connection-scope.test.ts +0 -266
- package/src/connection-scope.ts +0 -314
- package/src/credential-less-sync-fence.test.ts +0 -152
- package/src/e2e-processor-shim.ts +0 -232
- package/src/emit.test.ts +0 -145
- package/src/emit.ts +0 -115
- package/src/events.ts +0 -252
- package/src/failure-kind.test.ts +0 -42
- package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
- package/src/handlers/append-sent-message.ts +0 -264
- package/src/handlers/delete-account-objects.test.ts +0 -126
- package/src/handlers/delete-account-objects.ts +0 -96
- package/src/handlers/empty-trash.sqlite.test.ts +0 -209
- package/src/handlers/empty-trash.ts +0 -352
- package/src/handlers/flag-push.ts +0 -285
- package/src/handlers/imported-folder.test.ts +0 -63
- package/src/handlers/imported-folder.ts +0 -38
- package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
- package/src/handlers/mailbox-management.ts +0 -486
- package/src/handlers/mailbox-sync-order.test.ts +0 -93
- package/src/handlers/mailbox-sync-order.ts +0 -65
- package/src/handlers/message-copy.sqlite.test.ts +0 -298
- package/src/handlers/message-copy.ts +0 -569
- package/src/handlers/message-delete-terminal.ts +0 -147
- package/src/handlers/message-delete.sqlite.test.ts +0 -291
- package/src/handlers/message-delete.ts +0 -888
- package/src/handlers/message-move.sqlite.test.ts +0 -271
- package/src/handlers/message-move.test.ts +0 -216
- package/src/handlers/message-move.ts +0 -755
- package/src/handlers/placement-move-push.test.ts +0 -137
- package/src/handlers/placement-move-push.ts +0 -478
- package/src/handlers/placement-push.sqlite.test.ts +0 -299
- package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
- package/src/handlers/probe-message-id.ts +0 -15
- package/src/handlers/sync-mailboxes.test.ts +0 -183
- package/src/handlers/sync-mailboxes.ts +0 -425
- package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
- package/src/handlers/sync-message-body.test.ts +0 -150
- package/src/handlers/sync-message-body.ts +0 -366
- package/src/handlers/sync-messages.sqlite.test.ts +0 -195
- package/src/handlers/sync-messages.ts +0 -537
- package/src/index.test.ts +0 -22
- package/src/index.ts +0 -82
- package/src/mail-sync-selection.sqlite.test.ts +0 -227
- package/src/mailbox-row.ts +0 -20
- package/src/pending-sync.sqlite.test.ts +0 -165
- package/src/poller.ts +0 -55
- package/src/processor.ts +0 -62
- package/src/queues.ts +0 -40
- package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
- package/src/scheduler/attachment-sweep.ts +0 -46
- package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
- package/src/scheduler/calendar-subscriptions.ts +0 -103
- package/src/scheduler/config.test.ts +0 -40
- package/src/scheduler/decide-due.test.ts +0 -44
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
- package/src/scheduler/handler.ts +0 -69
- package/src/scheduler/loop.test.ts +0 -143
- package/src/scheduler/run-tick.sqlite.test.ts +0 -191
- package/src/scheduler/run-tick.ts +0 -281
- package/src/scheduler/runner.ts +0 -125
- package/src/scheduler/stranded-placement-redrive.ts +0 -265
- package/src/stranded-placement.sqlite.test.ts +0 -799
- package/src/test-helpers/test-world.ts +0 -425
- package/src/test-helpers/worker-queues.ts +0 -11
- package/src/thread-join.sqlite.test.ts +0 -106
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
- package/src/with-oauth-lifecycle-deps.ts +0 -71
- package/src/with-oauth-lifecycle.ts +0 -158
- 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
|
-
};
|