@remit/backend 0.0.142 → 0.0.144
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/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +7 -0
- package/dist/src/api.js +144 -0
- package/dist/src/auth.d.ts +30 -0
- package/dist/src/auth.js +73 -0
- package/dist/src/config/msoauth.d.ts +30 -0
- package/dist/src/config/msoauth.js +30 -0
- package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
- package/dist/src/data-backend.js +12 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +41 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
- package/dist/src/derive/contentUrl.d.ts +46 -0
- package/dist/src/derive/contentUrl.js +46 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +14 -0
- package/dist/src/handlers/account-oauth.js +387 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +6 -0
- package/dist/src/handlers/account.js +350 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +141 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +425 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +192 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +212 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +330 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +176 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +103 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +40 -0
- package/dist/src/handlers/index.d.ts +2 -0
- package/dist/src/handlers/index.js +60 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +114 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +219 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +138 -0
- package/dist/src/handlers/message.d.ts +84 -0
- package/dist/src/handlers/message.js +607 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +112 -0
- package/dist/src/handlers/outbox-attachment.d.ts +23 -0
- package/dist/src/handlers/outbox-attachment.js +81 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +144 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +82 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +172 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +233 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +409 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +37 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +18 -0
- package/dist/src/jwt-auth.js +97 -0
- package/dist/src/request-context.d.ts +9 -0
- package/dist/src/request-context.js +22 -0
- package/dist/src/request.d.ts +3 -0
- package/dist/src/request.js +7 -0
- package/dist/src/response.d.ts +27 -0
- package/dist/src/response.js +87 -0
- package/dist/src/service/account-deletion.d.ts +22 -0
- package/dist/src/service/account-deletion.js +39 -0
- package/dist/src/service/compose-sqlite.d.ts +2 -0
- package/dist/src/service/compose-sqlite.js +11 -0
- package/dist/src/service/create-remit-client.d.ts +95 -0
- package/dist/src/service/create-remit-client.js +207 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +44 -0
- package/dist/src/service/export-identity.d.ts +3 -0
- package/dist/src/service/export-identity.js +35 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +38 -0
- package/dist/src/service/organize.d.ts +184 -0
- package/dist/src/service/organize.js +384 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/sqs.d.ts +1 -0
- package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
- package/dist/src/service/trigger-sync.d.ts +51 -0
- package/dist/src/service/trigger-sync.js +39 -0
- package/dist/src/types.d.ts +49 -0
- package/dist/src/types.js +1 -0
- package/package.json +113 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -521
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { BadRequestError, NotFoundError } from "@remit/data-ports/errors";
|
|
2
|
+
import { logger } from "@remit/logger";
|
|
3
|
+
import { ownedAccount, queueOrganizeJob, } from "@remit/mailbox-service";
|
|
4
|
+
import { env } from "expect-env";
|
|
5
|
+
import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
|
|
6
|
+
import { getClient } from "../service/data-client.js";
|
|
7
|
+
import { buildOrganizeMatchDeps, matchOrganize, ORGANIZE_MATCH_LIMIT, organizePredicateRejection, } from "../service/organize.js";
|
|
8
|
+
import { unwrap } from "../service/refusal.js";
|
|
9
|
+
/**
|
|
10
|
+
* Normalize the request body into the flattened predicate the job row and the
|
|
11
|
+
* matcher share.
|
|
12
|
+
*/
|
|
13
|
+
export const predicateFromInput = (input) => ({
|
|
14
|
+
matchOperator: input.matchOperator,
|
|
15
|
+
literalClauses: input.literalClauses,
|
|
16
|
+
actionLabelId: input.actionLabelId,
|
|
17
|
+
actionMailboxId: input.actionMailboxId,
|
|
18
|
+
});
|
|
19
|
+
const assertAccount = async (client, accountId, accountConfigId, mode) => {
|
|
20
|
+
const account = await client.account.get(accountId);
|
|
21
|
+
unwrap(ownedAccount(account, accountConfigId, mode));
|
|
22
|
+
};
|
|
23
|
+
const toOrganizeJobResponse = (job) => ({
|
|
24
|
+
organizeJobId: job.organizeJobId,
|
|
25
|
+
accountConfigId: job.accountConfigId,
|
|
26
|
+
userId: job.userId,
|
|
27
|
+
state: job.state,
|
|
28
|
+
filterId: job.filterId,
|
|
29
|
+
matchOperator: job.matchOperator,
|
|
30
|
+
literalClauses: job.literalClauses,
|
|
31
|
+
actionLabelId: job.actionLabelId,
|
|
32
|
+
actionMailboxId: job.actionMailboxId,
|
|
33
|
+
matchedCount: job.matchedCount,
|
|
34
|
+
appliedCount: job.appliedCount,
|
|
35
|
+
failedCount: job.failedCount,
|
|
36
|
+
errorMessage: job.errorMessage,
|
|
37
|
+
createdAt: job.createdAt,
|
|
38
|
+
updatedAt: job.updatedAt,
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* What a create asks the worker to run. A saved filter is named and read when
|
|
42
|
+
* the job runs, so Run now and a filter's create share one path (#1354); an
|
|
43
|
+
* "all like these" predicate the matcher can never honour is answered here,
|
|
44
|
+
* not with a 202 the worker has to fail later (reader #463).
|
|
45
|
+
*/
|
|
46
|
+
const requestedJob = async (client, owner, input) => {
|
|
47
|
+
if (input.filterId) {
|
|
48
|
+
await client.filter.get(owner.accountConfigId, input.filterId);
|
|
49
|
+
return { ...owner, filterId: input.filterId };
|
|
50
|
+
}
|
|
51
|
+
const predicate = predicateFromInput(input);
|
|
52
|
+
const rejection = organizePredicateRejection(predicate);
|
|
53
|
+
if (rejection)
|
|
54
|
+
throw new BadRequestError(rejection.message);
|
|
55
|
+
return { ...owner, predicate };
|
|
56
|
+
};
|
|
57
|
+
export const OrganizeOperations = {
|
|
58
|
+
OrganizeOperations_createOrganizeJob: async (context, ...args) => {
|
|
59
|
+
const event = args[0];
|
|
60
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
61
|
+
const { accountId } = context.request.params;
|
|
62
|
+
const input = context.request.requestBody;
|
|
63
|
+
const userId = getSubFromEvent(event);
|
|
64
|
+
if (!userId) {
|
|
65
|
+
throw new Error("Missing Cognito `sub`: cannot attribute an organize job to a user");
|
|
66
|
+
}
|
|
67
|
+
const client = await getClient();
|
|
68
|
+
await assertAccount(client, accountId, accountConfigId, "act");
|
|
69
|
+
const job = await queueOrganizeJob({
|
|
70
|
+
organizeJobRequest: client.organizeJobRequest,
|
|
71
|
+
sqsQueueUrl: env.SQS_QUEUE_URL_ACCOUNT_FANOUT,
|
|
72
|
+
logger,
|
|
73
|
+
}, await requestedJob(client, { accountConfigId, userId }, input));
|
|
74
|
+
return {
|
|
75
|
+
statusCode: 202,
|
|
76
|
+
organizeJobId: job.organizeJobId,
|
|
77
|
+
state: job.state,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
OrganizeOperations_previewOrganize: async (context, ...args) => {
|
|
81
|
+
const event = args[0];
|
|
82
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
83
|
+
const { accountId } = context.request.params;
|
|
84
|
+
const input = context.request.requestBody;
|
|
85
|
+
const client = await getClient();
|
|
86
|
+
await assertAccount(client, accountId, accountConfigId, "read");
|
|
87
|
+
if (input.filterId) {
|
|
88
|
+
throw new BadRequestError("Preview takes a rule, not a saved filter. Run the filter to apply it.");
|
|
89
|
+
}
|
|
90
|
+
const result = await matchOrganize(buildOrganizeMatchDeps(client), accountConfigId, predicateFromInput(input), ORGANIZE_MATCH_LIMIT);
|
|
91
|
+
if (result.rejected)
|
|
92
|
+
throw new BadRequestError(result.rejected.message);
|
|
93
|
+
return {
|
|
94
|
+
matchedCount: result.messageIds.length,
|
|
95
|
+
messageIds: result.messageIds,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
export const OrganizeJobDetailOperations = {
|
|
100
|
+
OrganizeJobDetailOperations_getOrganizeJob: async (context, ...args) => {
|
|
101
|
+
const event = args[0];
|
|
102
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
103
|
+
const { accountId, organizeJobId } = context.request.params;
|
|
104
|
+
const client = await getClient();
|
|
105
|
+
await assertAccount(client, accountId, accountConfigId, "read");
|
|
106
|
+
const job = await client.organizeJobRequest.get(organizeJobId);
|
|
107
|
+
if (job.accountConfigId !== accountConfigId) {
|
|
108
|
+
throw new NotFoundError(`Organize job not found: ${organizeJobId}`);
|
|
109
|
+
}
|
|
110
|
+
return toOrganizeJobResponse(job);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MintOutboxAttachmentResponse, OutboxAttachmentRejection, OutboxAttachmentResponse } from "@remit/api-openapi-types";
|
|
2
|
+
import type { OutboxAttachmentItem } from "@remit/data-ports";
|
|
3
|
+
import type { Context } from "openapi-backend";
|
|
4
|
+
export declare const toOutboxAttachmentResponse: (item: OutboxAttachmentItem) => OutboxAttachmentResponse;
|
|
5
|
+
interface RejectionResponse {
|
|
6
|
+
statusCode: number;
|
|
7
|
+
body: OutboxAttachmentRejection;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reserve room on a draft and hand back somewhere to put the bytes. Small JSON
|
|
11
|
+
* in, small JSON out — the file itself never passes through here, which is what
|
|
12
|
+
* lets the upload live on its own fleet and keeps this operation under any
|
|
13
|
+
* payload ceiling the REST tier has.
|
|
14
|
+
*/
|
|
15
|
+
export declare const mintOutboxAttachment: (context: Context, ...args: unknown[]) => Promise<MintOutboxAttachmentResponse | RejectionResponse>;
|
|
16
|
+
/**
|
|
17
|
+
* Confirm the upload landed. The size in the response is read back from
|
|
18
|
+
* storage — on a deployment where the browser PUT straight to block storage
|
|
19
|
+
* this call is the first the API hears of the bytes, and the client's word for
|
|
20
|
+
* how many there are is not evidence.
|
|
21
|
+
*/
|
|
22
|
+
export declare const completeOutboxAttachment: (context: Context, ...args: unknown[]) => Promise<OutboxAttachmentResponse | RejectionResponse>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { OutboxAttachmentRejectionReason } from "@remit/domain-enums";
|
|
2
|
+
import { outboxAttachmentContentId } from "@remit/mailbox-service/outbox-attachment-content";
|
|
3
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
4
|
+
import { getContentSigner } from "../derive/contentSignature.js";
|
|
5
|
+
import { buildStorageContentUrl, getContentDeliveryDomain, } from "../derive/contentUrl.js";
|
|
6
|
+
import { getClient } from "../service/data-client.js";
|
|
7
|
+
// Every error body the API emits carries a `code` (issue #371). A refusal here
|
|
8
|
+
// is one kind, so the code is one value and `reason` says which refusal it was.
|
|
9
|
+
const ATTACHMENT_REJECTED_CODE = "attachment_rejected";
|
|
10
|
+
const TOO_LARGE = new Set([
|
|
11
|
+
OutboxAttachmentRejectionReason.FileTooLarge,
|
|
12
|
+
OutboxAttachmentRejectionReason.MessageTooLarge,
|
|
13
|
+
OutboxAttachmentRejectionReason.TooManyAttachments,
|
|
14
|
+
]);
|
|
15
|
+
export const toOutboxAttachmentResponse = (item) => ({
|
|
16
|
+
outboxAttachmentId: item.outboxAttachmentId,
|
|
17
|
+
outboxMessageId: item.outboxMessageId,
|
|
18
|
+
filename: item.filename,
|
|
19
|
+
contentType: item.contentType,
|
|
20
|
+
sizeBytes: item.sizeBytes,
|
|
21
|
+
state: item.state,
|
|
22
|
+
contentId: outboxAttachmentContentId(item.outboxAttachmentId),
|
|
23
|
+
contentUrl: buildStorageContentUrl({
|
|
24
|
+
domain: getContentDeliveryDomain(),
|
|
25
|
+
storageKey: item.storageKey,
|
|
26
|
+
sign: getContentSigner(),
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
const refuse = (detail) => ({
|
|
30
|
+
statusCode: TOO_LARGE.has(detail.reason) ? 413 : 400,
|
|
31
|
+
body: {
|
|
32
|
+
code: ATTACHMENT_REJECTED_CODE,
|
|
33
|
+
reason: detail.reason,
|
|
34
|
+
message: detail.message,
|
|
35
|
+
limitBytes: detail.limitBytes,
|
|
36
|
+
usedBytes: detail.usedBytes,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Reserve room on a draft and hand back somewhere to put the bytes. Small JSON
|
|
41
|
+
* in, small JSON out — the file itself never passes through here, which is what
|
|
42
|
+
* lets the upload live on its own fleet and keeps this operation under any
|
|
43
|
+
* payload ceiling the REST tier has.
|
|
44
|
+
*/
|
|
45
|
+
export const mintOutboxAttachment = async (context, ...args) => {
|
|
46
|
+
const event = args[0];
|
|
47
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
48
|
+
const { outboxMessageId } = context.request.params;
|
|
49
|
+
const input = context.request.requestBody;
|
|
50
|
+
const client = await getClient();
|
|
51
|
+
const result = await client.outboxAttachment.mint({
|
|
52
|
+
accountConfigId,
|
|
53
|
+
outboxMessageId,
|
|
54
|
+
filename: input.filename,
|
|
55
|
+
contentType: input.contentType,
|
|
56
|
+
sizeBytes: input.sizeBytes,
|
|
57
|
+
});
|
|
58
|
+
if (result.outcome === "Rejected")
|
|
59
|
+
return refuse(result.rejection);
|
|
60
|
+
return result.reservation;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Confirm the upload landed. The size in the response is read back from
|
|
64
|
+
* storage — on a deployment where the browser PUT straight to block storage
|
|
65
|
+
* this call is the first the API hears of the bytes, and the client's word for
|
|
66
|
+
* how many there are is not evidence.
|
|
67
|
+
*/
|
|
68
|
+
export const completeOutboxAttachment = async (context, ...args) => {
|
|
69
|
+
const event = args[0];
|
|
70
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
71
|
+
const { outboxMessageId, outboxAttachmentId } = context.request.params;
|
|
72
|
+
const client = await getClient();
|
|
73
|
+
const result = await client.outboxAttachment.complete({
|
|
74
|
+
accountConfigId,
|
|
75
|
+
outboxMessageId,
|
|
76
|
+
outboxAttachmentId,
|
|
77
|
+
});
|
|
78
|
+
if (result.outcome === "Rejected")
|
|
79
|
+
return refuse(result.rejection);
|
|
80
|
+
return toOutboxAttachmentResponse(result.attachment);
|
|
81
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OperationHandler, OutboxAttachmentOperationIds, OutboxDetailOperationIds, OutboxOperationIds } from "../types.js";
|
|
2
|
+
export declare const OutboxOperations: Record<OutboxOperationIds, OperationHandler<OutboxOperationIds>>;
|
|
3
|
+
export declare const OutboxDetailOperations: Record<OutboxDetailOperationIds, OperationHandler<OutboxDetailOperationIds>>;
|
|
4
|
+
export declare const OutboxAttachmentOperations: Record<OutboxAttachmentOperationIds, OperationHandler<OutboxAttachmentOperationIds>>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ForbiddenError } from "@remit/data-ports/errors";
|
|
2
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
3
|
+
import { getClient } from "../service/data-client.js";
|
|
4
|
+
import { completeOutboxAttachment, mintOutboxAttachment, toOutboxAttachmentResponse, } from "./outbox-attachment.js";
|
|
5
|
+
const toOutboxMessageResponse = (item, attachments = []) => ({
|
|
6
|
+
attachments: attachments.map(toOutboxAttachmentResponse),
|
|
7
|
+
outboxMessageId: item.outboxMessageId,
|
|
8
|
+
accountId: item.accountId,
|
|
9
|
+
fromAddress: item.fromAddress,
|
|
10
|
+
fromName: item.fromName,
|
|
11
|
+
toAddresses: item.toAddresses,
|
|
12
|
+
ccAddresses: item.ccAddresses ?? [],
|
|
13
|
+
bccAddresses: item.bccAddresses ?? [],
|
|
14
|
+
subject: item.subject,
|
|
15
|
+
textBody: item.textBody,
|
|
16
|
+
htmlBody: item.htmlBody,
|
|
17
|
+
inReplyTo: item.inReplyTo,
|
|
18
|
+
references: item.references ?? [],
|
|
19
|
+
status: item.status,
|
|
20
|
+
lastError: item.lastError,
|
|
21
|
+
sentAt: item.sentAt,
|
|
22
|
+
createdAt: item.createdAt,
|
|
23
|
+
updatedAt: item.updatedAt,
|
|
24
|
+
});
|
|
25
|
+
export const OutboxOperations = {
|
|
26
|
+
OutboxOperations_createOutboxMessage: async (_context, ...args) => {
|
|
27
|
+
const event = args[0];
|
|
28
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
29
|
+
const input = JSON.parse(event.body ?? "{}");
|
|
30
|
+
const client = await getClient();
|
|
31
|
+
const account = await client.account.get(input.accountId);
|
|
32
|
+
if (account.accountConfigId !== accountConfigId) {
|
|
33
|
+
throw new ForbiddenError(`Account ${input.accountId} not in account config`);
|
|
34
|
+
}
|
|
35
|
+
const fromAddress = account.email;
|
|
36
|
+
if (input.sendImmediately) {
|
|
37
|
+
const outbox = await client.outboxQueue.createAndSend({
|
|
38
|
+
accountId: input.accountId,
|
|
39
|
+
accountConfigId,
|
|
40
|
+
fromAddress,
|
|
41
|
+
toAddresses: input.toAddresses,
|
|
42
|
+
ccAddresses: input.ccAddresses,
|
|
43
|
+
bccAddresses: input.bccAddresses,
|
|
44
|
+
subject: input.subject,
|
|
45
|
+
textBody: input.textBody,
|
|
46
|
+
htmlBody: input.htmlBody,
|
|
47
|
+
inReplyTo: input.inReplyTo,
|
|
48
|
+
references: input.references,
|
|
49
|
+
});
|
|
50
|
+
return toOutboxMessageResponse(outbox);
|
|
51
|
+
}
|
|
52
|
+
const outbox = await client.outboxQueue.createDraft({
|
|
53
|
+
accountId: input.accountId,
|
|
54
|
+
accountConfigId,
|
|
55
|
+
fromAddress,
|
|
56
|
+
toAddresses: input.toAddresses,
|
|
57
|
+
ccAddresses: input.ccAddresses,
|
|
58
|
+
bccAddresses: input.bccAddresses,
|
|
59
|
+
subject: input.subject,
|
|
60
|
+
textBody: input.textBody,
|
|
61
|
+
htmlBody: input.htmlBody,
|
|
62
|
+
inReplyTo: input.inReplyTo,
|
|
63
|
+
references: input.references,
|
|
64
|
+
});
|
|
65
|
+
return toOutboxMessageResponse(outbox);
|
|
66
|
+
},
|
|
67
|
+
OutboxOperations_listOutboxMessages: async (context, ...args) => {
|
|
68
|
+
const event = args[0];
|
|
69
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
70
|
+
const { continuationToken } = context.request.query;
|
|
71
|
+
const client = await getClient();
|
|
72
|
+
const accounts = await client.account.listAllByAccountConfig(accountConfigId);
|
|
73
|
+
if (accounts.length === 0) {
|
|
74
|
+
return { items: [], continuationToken: null };
|
|
75
|
+
}
|
|
76
|
+
const result = await client.outboxMessage.listByAccounts(accounts.map((account) => account.accountId), { continuationToken });
|
|
77
|
+
return {
|
|
78
|
+
items: result.items.map((item) => toOutboxMessageResponse(item)),
|
|
79
|
+
continuationToken: result.continuationToken,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
export const OutboxDetailOperations = {
|
|
84
|
+
OutboxDetailOperations_getOutboxMessage: async (context, ...args) => {
|
|
85
|
+
const event = args[0];
|
|
86
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
87
|
+
const { outboxMessageId } = context.request.params;
|
|
88
|
+
const client = await getClient();
|
|
89
|
+
// Default mode "read": the scoped get refuses a foreign message with
|
|
90
|
+
// NotFound (404, no existence leak) — no separate check needed.
|
|
91
|
+
const outbox = await client.outboxMessage.get(accountConfigId, outboxMessageId);
|
|
92
|
+
return toOutboxMessageResponse(outbox, await client.outboxAttachment.listFor(accountConfigId, outboxMessageId));
|
|
93
|
+
},
|
|
94
|
+
OutboxDetailOperations_updateOutboxMessage: async (context, ...args) => {
|
|
95
|
+
const event = args[0];
|
|
96
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
97
|
+
const { outboxMessageId } = context.request.params;
|
|
98
|
+
const input = context.request.requestBody;
|
|
99
|
+
const client = await getClient();
|
|
100
|
+
// updateDraft's own scoped get uses mode "act": a foreign message is
|
|
101
|
+
// denied with Forbidden (403), not feigned as NotFound.
|
|
102
|
+
const updated = await client.outboxQueue.updateDraft(accountConfigId, outboxMessageId, {
|
|
103
|
+
toAddresses: input.toAddresses,
|
|
104
|
+
ccAddresses: input.ccAddresses,
|
|
105
|
+
bccAddresses: input.bccAddresses,
|
|
106
|
+
subject: input.subject,
|
|
107
|
+
textBody: input.textBody,
|
|
108
|
+
htmlBody: input.htmlBody,
|
|
109
|
+
inReplyTo: input.inReplyTo,
|
|
110
|
+
references: input.references,
|
|
111
|
+
});
|
|
112
|
+
// Absent means "leave the files alone" — a body that only carries a subject
|
|
113
|
+
// must not strip them. Present states exactly what the draft keeps, so an
|
|
114
|
+
// empty list is a real instruction to remove them all.
|
|
115
|
+
if (input.attachmentIds !== undefined) {
|
|
116
|
+
await client.outboxAttachment.retainOnly(accountConfigId, updated.accountId, outboxMessageId, input.attachmentIds);
|
|
117
|
+
}
|
|
118
|
+
return toOutboxMessageResponse(updated, await client.outboxAttachment.listFor(accountConfigId, outboxMessageId));
|
|
119
|
+
},
|
|
120
|
+
OutboxDetailOperations_deleteOutboxMessage: async (context, ...args) => {
|
|
121
|
+
const event = args[0];
|
|
122
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
123
|
+
const { outboxMessageId } = context.request.params;
|
|
124
|
+
const client = await getClient();
|
|
125
|
+
// deleteDraft's own scoped get uses mode "act": a foreign message is
|
|
126
|
+
// denied with Forbidden (403), not feigned as NotFound.
|
|
127
|
+
await client.outboxQueue.deleteDraft(accountConfigId, outboxMessageId);
|
|
128
|
+
return { statusCode: 204 };
|
|
129
|
+
},
|
|
130
|
+
OutboxDetailOperations_sendOutboxMessage: async (context, ...args) => {
|
|
131
|
+
const event = args[0];
|
|
132
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
133
|
+
const { outboxMessageId } = context.request.params;
|
|
134
|
+
const client = await getClient();
|
|
135
|
+
// send's own scoped get uses mode "act": a foreign message is denied
|
|
136
|
+
// with Forbidden (403), not feigned as NotFound.
|
|
137
|
+
const sent = await client.outboxQueue.send(accountConfigId, outboxMessageId);
|
|
138
|
+
return toOutboxMessageResponse(sent);
|
|
139
|
+
},
|
|
140
|
+
OutboxDetailOperations_mintOutboxAttachment: mintOutboxAttachment,
|
|
141
|
+
};
|
|
142
|
+
export const OutboxAttachmentOperations = {
|
|
143
|
+
OutboxAttachmentOperations_completeOutboxAttachment: completeOutboxAttachment,
|
|
144
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { MailboxItem } from "@remit/data-ports";
|
|
2
|
+
/** The subset of mailbox fields needed to derive sync progress. */
|
|
3
|
+
export type MailboxSyncFields = Pick<MailboxItem, "lastMessageSyncAt" | "initialSyncCompletedAt" | "messageCount" | "lastSyncUid" | "highWaterMarkUid">;
|
|
4
|
+
/** A mailbox row, as far as the sync-status projection reads it. */
|
|
5
|
+
export type MailboxProgressFields = MailboxSyncFields & Pick<MailboxItem, "mailboxId" | "fullPath">;
|
|
6
|
+
/** The per-mailbox entry `getSyncStatus` returns. */
|
|
7
|
+
export interface MailboxSyncProgress {
|
|
8
|
+
mailboxId: string;
|
|
9
|
+
fullPath: string;
|
|
10
|
+
phase: string;
|
|
11
|
+
messagesTotal: number;
|
|
12
|
+
messagesSynced: number;
|
|
13
|
+
highWaterMarkUid: number;
|
|
14
|
+
lastSyncedAt: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Derive a per-mailbox sync phase from worker-written sync state.
|
|
18
|
+
*
|
|
19
|
+
* - pending: never synced (lastMessageSyncAt is 0 / unset)
|
|
20
|
+
* - complete: the imap-worker drained the mailbox and stamped
|
|
21
|
+
* initialSyncCompletedAt after the last batch write
|
|
22
|
+
* (initialSyncCompletedAt >= lastMessageSyncAt), or the
|
|
23
|
+
* mailbox is empty
|
|
24
|
+
* - syncing: sync has started but is not yet complete
|
|
25
|
+
*
|
|
26
|
+
* Note: UID watermarks alone cannot express completion — backfill settles
|
|
27
|
+
* at the mailbox's smallest real UID (UIDs are sparse), so the completion
|
|
28
|
+
* marker written by the worker is the source of truth.
|
|
29
|
+
*/
|
|
30
|
+
export declare const deriveMailboxPhase: (mailbox: MailboxSyncFields) => string;
|
|
31
|
+
/**
|
|
32
|
+
* Compute approximate messagesSynced from UID watermarks.
|
|
33
|
+
*
|
|
34
|
+
* UIDs are sparse (not necessarily contiguous), so this is an approximation.
|
|
35
|
+
* We clamp the result to [0, messagesTotal].
|
|
36
|
+
*/
|
|
37
|
+
export declare const computeMessagesSynced: (mailbox: MailboxSyncFields) => number;
|
|
38
|
+
/**
|
|
39
|
+
* Project a mailbox row into its sync-status entry.
|
|
40
|
+
*
|
|
41
|
+
* `highWaterMarkUid` is carried through because a total cannot tell an arrival
|
|
42
|
+
* from a departure: a message landing in the same sync round that another one
|
|
43
|
+
* leaves nets to zero, and the client's freshness check saw nothing (#771). The
|
|
44
|
+
* highest UID only ever moves up, so it names the arrival on its own.
|
|
45
|
+
*/
|
|
46
|
+
export declare const toMailboxSyncProgress: (mailbox: MailboxProgressFields) => MailboxSyncProgress;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a per-mailbox sync phase from worker-written sync state.
|
|
3
|
+
*
|
|
4
|
+
* - pending: never synced (lastMessageSyncAt is 0 / unset)
|
|
5
|
+
* - complete: the imap-worker drained the mailbox and stamped
|
|
6
|
+
* initialSyncCompletedAt after the last batch write
|
|
7
|
+
* (initialSyncCompletedAt >= lastMessageSyncAt), or the
|
|
8
|
+
* mailbox is empty
|
|
9
|
+
* - syncing: sync has started but is not yet complete
|
|
10
|
+
*
|
|
11
|
+
* Note: UID watermarks alone cannot express completion — backfill settles
|
|
12
|
+
* at the mailbox's smallest real UID (UIDs are sparse), so the completion
|
|
13
|
+
* marker written by the worker is the source of truth.
|
|
14
|
+
*/
|
|
15
|
+
export const deriveMailboxPhase = (mailbox) => {
|
|
16
|
+
if (!mailbox.lastMessageSyncAt)
|
|
17
|
+
return "pending";
|
|
18
|
+
const messageCount = mailbox.messageCount ?? 0;
|
|
19
|
+
const initialSyncCompletedAt = mailbox.initialSyncCompletedAt ?? 0;
|
|
20
|
+
// Completion marker is fresh: no batch was written after the worker
|
|
21
|
+
// recorded the mailbox as drained.
|
|
22
|
+
if (initialSyncCompletedAt > 0 &&
|
|
23
|
+
initialSyncCompletedAt >= mailbox.lastMessageSyncAt) {
|
|
24
|
+
return "complete";
|
|
25
|
+
}
|
|
26
|
+
// Also complete if messageCount is 0 (empty mailbox)
|
|
27
|
+
if (messageCount === 0)
|
|
28
|
+
return "complete";
|
|
29
|
+
return "syncing";
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Compute approximate messagesSynced from UID watermarks.
|
|
33
|
+
*
|
|
34
|
+
* UIDs are sparse (not necessarily contiguous), so this is an approximation.
|
|
35
|
+
* We clamp the result to [0, messagesTotal].
|
|
36
|
+
*/
|
|
37
|
+
export const computeMessagesSynced = (mailbox) => {
|
|
38
|
+
const messagesTotal = mailbox.messageCount ?? 0;
|
|
39
|
+
const lastSyncUid = mailbox.lastSyncUid ?? 0;
|
|
40
|
+
const highWaterMarkUid = mailbox.highWaterMarkUid ?? 0;
|
|
41
|
+
if (highWaterMarkUid === 0)
|
|
42
|
+
return 0;
|
|
43
|
+
// UID range approximation: number of UIDs in the synced range
|
|
44
|
+
const synced = highWaterMarkUid - lastSyncUid + 1;
|
|
45
|
+
return Math.min(Math.max(synced, 0), messagesTotal);
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Project a mailbox row into its sync-status entry.
|
|
49
|
+
*
|
|
50
|
+
* `highWaterMarkUid` is carried through because a total cannot tell an arrival
|
|
51
|
+
* from a departure: a message landing in the same sync round that another one
|
|
52
|
+
* leaves nets to zero, and the client's freshness check saw nothing (#771). The
|
|
53
|
+
* highest UID only ever moves up, so it names the arrival on its own.
|
|
54
|
+
*/
|
|
55
|
+
export const toMailboxSyncProgress = (mailbox) => ({
|
|
56
|
+
mailboxId: mailbox.mailboxId,
|
|
57
|
+
fullPath: mailbox.fullPath,
|
|
58
|
+
phase: deriveMailboxPhase(mailbox),
|
|
59
|
+
messagesTotal: mailbox.messageCount ?? 0,
|
|
60
|
+
messagesSynced: computeMessagesSynced(mailbox),
|
|
61
|
+
highWaterMarkUid: mailbox.highWaterMarkUid ?? 0,
|
|
62
|
+
lastSyncedAt: mailbox.lastMessageSyncAt || undefined,
|
|
63
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { type FireAndForgetLogger } from "../service/fire-and-forget.js";
|
|
3
|
+
import type { OperationHandler, SyncOperationIds } from "../types.js";
|
|
4
|
+
interface SyncTriggerDeps {
|
|
5
|
+
sqsClient: SQSClient;
|
|
6
|
+
queueUrl: string;
|
|
7
|
+
logger: FireAndForgetLogger & {
|
|
8
|
+
info: (fields: Record<string, unknown>, message: string) => void;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fire-and-forget enqueue for the POST /sync kick.
|
|
13
|
+
*
|
|
14
|
+
* POST /sync is a best-effort nudge: the web client polls it to wake a sync and
|
|
15
|
+
* acts on no enqueue result in the response. So the enqueue must never fail —
|
|
16
|
+
* nor leak onto — this request or any concurrent read when the queue is
|
|
17
|
+
* unreachable. Previously this handler `await`ed the SQS send directly; with the
|
|
18
|
+
* queue down (smoke/e2e, or an SQS outage in prod) the awaited rejection escaped
|
|
19
|
+
* the handler's promise and landed on whatever read was in flight on the shared
|
|
20
|
+
* event loop, 500-ing unrelated `/mailboxes`, `/threads` and `/outbox` reads.
|
|
21
|
+
*
|
|
22
|
+
* Routing it through fireAndForget catches the rejection at the source, logs it
|
|
23
|
+
* loudly with the alertable structured fields, and resolves to void. A genuine
|
|
24
|
+
* account-lookup / ownership failure still propagates and 500s as normal.
|
|
25
|
+
*/
|
|
26
|
+
export declare const triggerSyncSafe: (accountId: string, accountConfigId: string, deps?: SyncTriggerDeps) => Promise<void>;
|
|
27
|
+
export declare const SyncOperations: Record<SyncOperationIds, OperationHandler<SyncOperationIds>>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { logger } from "@remit/logger";
|
|
2
|
+
import { ownedAccount } from "@remit/mailbox-service";
|
|
3
|
+
import { env } from "expect-env";
|
|
4
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
5
|
+
import { getClient } from "../service/data-client.js";
|
|
6
|
+
import { fireAndForget, } from "../service/fire-and-forget.js";
|
|
7
|
+
import { unwrap } from "../service/refusal.js";
|
|
8
|
+
import { sqsClient } from "../service/sqs.js";
|
|
9
|
+
import { triggerAccountSync } from "../service/trigger-sync.js";
|
|
10
|
+
import { toMailboxSyncProgress } from "./sync-progress.js";
|
|
11
|
+
const defaultSyncTriggerDeps = () => ({
|
|
12
|
+
sqsClient,
|
|
13
|
+
queueUrl: env.SQS_QUEUE_URL,
|
|
14
|
+
logger,
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Fire-and-forget enqueue for the POST /sync kick.
|
|
18
|
+
*
|
|
19
|
+
* POST /sync is a best-effort nudge: the web client polls it to wake a sync and
|
|
20
|
+
* acts on no enqueue result in the response. So the enqueue must never fail —
|
|
21
|
+
* nor leak onto — this request or any concurrent read when the queue is
|
|
22
|
+
* unreachable. Previously this handler `await`ed the SQS send directly; with the
|
|
23
|
+
* queue down (smoke/e2e, or an SQS outage in prod) the awaited rejection escaped
|
|
24
|
+
* the handler's promise and landed on whatever read was in flight on the shared
|
|
25
|
+
* event loop, 500-ing unrelated `/mailboxes`, `/threads` and `/outbox` reads.
|
|
26
|
+
*
|
|
27
|
+
* Routing it through fireAndForget catches the rejection at the source, logs it
|
|
28
|
+
* loudly with the alertable structured fields, and resolves to void. A genuine
|
|
29
|
+
* account-lookup / ownership failure still propagates and 500s as normal.
|
|
30
|
+
*/
|
|
31
|
+
export const triggerSyncSafe = async (accountId, accountConfigId, deps = defaultSyncTriggerDeps()) => {
|
|
32
|
+
await fireAndForget(async () => {
|
|
33
|
+
const { eventId } = await triggerAccountSync({
|
|
34
|
+
sqsClient: deps.sqsClient,
|
|
35
|
+
queueUrl: deps.queueUrl,
|
|
36
|
+
accountId,
|
|
37
|
+
// POST /sync asks for a sync of this account by name — the refresh
|
|
38
|
+
// control, pull-to-refresh, and the client's automatic poll all land
|
|
39
|
+
// here — so it syncs every mailbox regardless of how recently one
|
|
40
|
+
// ran.
|
|
41
|
+
explicitRequest: true,
|
|
42
|
+
});
|
|
43
|
+
deps.logger.info({ accountId, eventId }, "Sync triggered - enqueued SYNC_MAILBOXES event");
|
|
44
|
+
}, {
|
|
45
|
+
source: "trigger_sync",
|
|
46
|
+
message: "Failed to enqueue SYNC_MAILBOXES on POST /sync (best-effort)",
|
|
47
|
+
ids: { accountId, accountConfigId },
|
|
48
|
+
logger: deps.logger,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
export const SyncOperations = {
|
|
52
|
+
SyncOperations_triggerSync: async (context, ...args) => {
|
|
53
|
+
const event = args[0];
|
|
54
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
55
|
+
const { accountId } = context.request.params;
|
|
56
|
+
const account = await (await getClient()).account.get(accountId);
|
|
57
|
+
unwrap(ownedAccount(account, accountConfigId, "act"));
|
|
58
|
+
void triggerSyncSafe(account.accountId, accountConfigId);
|
|
59
|
+
return {
|
|
60
|
+
triggered: true,
|
|
61
|
+
message: `Sync triggered for account ${accountId}`,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
SyncOperations_getSyncStatus: async (context, ...args) => {
|
|
65
|
+
const event = args[0];
|
|
66
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
67
|
+
const { accountId } = context.request.params;
|
|
68
|
+
const client = await getClient();
|
|
69
|
+
const account = await client.account.get(accountId);
|
|
70
|
+
unwrap(ownedAccount(account, accountConfigId, "read"));
|
|
71
|
+
// Collect all mailboxes (read-only — does not write or contend with sync lock)
|
|
72
|
+
const mailboxes = await client.mailbox.listAllByAccount(accountId);
|
|
73
|
+
return {
|
|
74
|
+
accountId: account.accountId,
|
|
75
|
+
syncPhase: account.syncPhase,
|
|
76
|
+
mailboxCountTotal: account.mailboxCountTotal,
|
|
77
|
+
mailboxCountSynced: account.mailboxCountSynced,
|
|
78
|
+
lastSyncAt: account.lastSyncAt,
|
|
79
|
+
mailboxes: mailboxes.map(toMailboxSyncProgress),
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|