@remit/backend 0.0.143 → 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 -522
- 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,114 @@
|
|
|
1
|
+
import { ownedAccount } from "@remit/mailbox-service";
|
|
2
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
3
|
+
import { getClient } from "../service/data-client.js";
|
|
4
|
+
import { unwrap } from "../service/refusal.js";
|
|
5
|
+
/**
|
|
6
|
+
* Every filter in the account whose action applies this label (issue #26). The
|
|
7
|
+
* count backs `LabelResponse.filterCount`; the list backs the cascade delete.
|
|
8
|
+
*/
|
|
9
|
+
export const findFiltersForLabel = async (deps, accountConfigId, labelId) => {
|
|
10
|
+
const filters = await deps.filter.listByAccountConfig(accountConfigId);
|
|
11
|
+
return filters.filter((filter) => filter.actionLabelId === labelId);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* `filterCount` is computed here, not stored (issue #26) — every other field
|
|
15
|
+
* comes straight off the `LabelItem` row.
|
|
16
|
+
*/
|
|
17
|
+
const toLabelResponse = (item, filterCount) => ({
|
|
18
|
+
labelId: item.labelId,
|
|
19
|
+
accountConfigId: item.accountConfigId,
|
|
20
|
+
name: item.name,
|
|
21
|
+
color: item.color,
|
|
22
|
+
filterCount,
|
|
23
|
+
createdAt: item.createdAt,
|
|
24
|
+
updatedAt: item.updatedAt,
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Delete a label and cascade in both directions (issue #26): every filter
|
|
28
|
+
* whose `actionLabelId` is this label is deleted outright — never left
|
|
29
|
+
* dangling, never blocking the delete — and every `MessageLabel` row for it is
|
|
30
|
+
* removed. The label row itself is deleted last so a failure mid-cascade never
|
|
31
|
+
* leaves the label gone while its filters or applied-to-messages still exist.
|
|
32
|
+
*/
|
|
33
|
+
export const deleteLabelWithCascade = async (deps, accountConfigId, labelId) => {
|
|
34
|
+
const filters = await findFiltersForLabel(deps, accountConfigId, labelId);
|
|
35
|
+
for (const filter of filters) {
|
|
36
|
+
await deps.filter.delete(accountConfigId, filter.filterId);
|
|
37
|
+
}
|
|
38
|
+
await deps.messageLabel.removeAllByLabelId(accountConfigId, labelId);
|
|
39
|
+
await deps.label.delete(accountConfigId, labelId);
|
|
40
|
+
};
|
|
41
|
+
export const LabelOperations = {
|
|
42
|
+
LabelOperations_listLabels: async (context, ...args) => {
|
|
43
|
+
const event = args[0];
|
|
44
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
45
|
+
const { accountId } = context.request.params;
|
|
46
|
+
const { continuationToken } = context.request.query;
|
|
47
|
+
const client = await getClient();
|
|
48
|
+
const account = await client.account.get(accountId);
|
|
49
|
+
unwrap(ownedAccount(account, accountConfigId, "read"));
|
|
50
|
+
const page = await client.label.listPageByAccountConfig(accountConfigId, {
|
|
51
|
+
continuationToken,
|
|
52
|
+
});
|
|
53
|
+
const filters = await client.filter.listByAccountConfig(accountConfigId);
|
|
54
|
+
const filterCountByLabelId = new Map();
|
|
55
|
+
for (const filter of filters) {
|
|
56
|
+
const labelId = filter.actionLabelId;
|
|
57
|
+
filterCountByLabelId.set(labelId, (filterCountByLabelId.get(labelId) ?? 0) + 1);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
items: page.items.map((item) => toLabelResponse(item, filterCountByLabelId.get(item.labelId) ?? 0)),
|
|
61
|
+
continuationToken: page.continuationToken,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
LabelOperations_createLabel: async (context, ...args) => {
|
|
65
|
+
const event = args[0];
|
|
66
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
67
|
+
const { accountId } = context.request.params;
|
|
68
|
+
const input = context.request.requestBody;
|
|
69
|
+
const client = await getClient();
|
|
70
|
+
const account = await client.account.get(accountId);
|
|
71
|
+
unwrap(ownedAccount(account, accountConfigId, "act"));
|
|
72
|
+
const label = await client.label.create({
|
|
73
|
+
accountConfigId,
|
|
74
|
+
name: input.name,
|
|
75
|
+
color: input.color,
|
|
76
|
+
});
|
|
77
|
+
return toLabelResponse(label, 0);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
export const LabelDetailOperations = {
|
|
81
|
+
LabelDetailOperations_getLabel: async (context, ...args) => {
|
|
82
|
+
const event = args[0];
|
|
83
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
84
|
+
const { accountId, labelId } = context.request.params;
|
|
85
|
+
const client = await getClient();
|
|
86
|
+
const account = await client.account.get(accountId);
|
|
87
|
+
unwrap(ownedAccount(account, accountConfigId, "read"));
|
|
88
|
+
const label = await client.label.get(accountConfigId, labelId);
|
|
89
|
+
const filters = await findFiltersForLabel(client, accountConfigId, labelId);
|
|
90
|
+
return toLabelResponse(label, filters.length);
|
|
91
|
+
},
|
|
92
|
+
LabelDetailOperations_updateLabel: async (context, ...args) => {
|
|
93
|
+
const event = args[0];
|
|
94
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
95
|
+
const { accountId, labelId } = context.request.params;
|
|
96
|
+
const body = context.request.requestBody;
|
|
97
|
+
const client = await getClient();
|
|
98
|
+
const account = await client.account.get(accountId);
|
|
99
|
+
unwrap(ownedAccount(account, accountConfigId, "act"));
|
|
100
|
+
const updated = await client.label.update(accountConfigId, labelId, body);
|
|
101
|
+
const filters = await findFiltersForLabel(client, accountConfigId, labelId);
|
|
102
|
+
return toLabelResponse(updated, filters.length);
|
|
103
|
+
},
|
|
104
|
+
LabelDetailOperations_deleteLabel: async (context, ...args) => {
|
|
105
|
+
const event = args[0];
|
|
106
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
107
|
+
const { accountId, labelId } = context.request.params;
|
|
108
|
+
const client = await getClient();
|
|
109
|
+
const account = await client.account.get(accountId);
|
|
110
|
+
unwrap(ownedAccount(account, accountConfigId, "act"));
|
|
111
|
+
await deleteLabelWithCascade(client, accountConfigId, labelId);
|
|
112
|
+
return { statusCode: 204 };
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IAccountSettingRepository, IFilterRepository, MailboxItem } from "@remit/data-ports";
|
|
2
|
+
import type { CanonicalMailboxRoleValue } from "@remit/data-ports/folder-role";
|
|
3
|
+
/**
|
|
4
|
+
* What a folder is bound to: the filters that move mail into it, and the
|
|
5
|
+
* canonical roles it has been appointed to.
|
|
6
|
+
*/
|
|
7
|
+
export interface MailboxBindings {
|
|
8
|
+
filterNames: string[];
|
|
9
|
+
roles: CanonicalMailboxRoleValue[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `filter.actionMailboxId` has no reverse index, so the filters are listed for
|
|
13
|
+
* the account config and matched — they are few and per-config, so this is not
|
|
14
|
+
* a mailbox scan. Role appointments are `account_setting` rows whose *value* is
|
|
15
|
+
* a mailboxId, which a `mailbox_id` column scan cannot find, so the existing
|
|
16
|
+
* per-account appointment map answers for them.
|
|
17
|
+
*/
|
|
18
|
+
export declare const loadMailboxBindings: (client: {
|
|
19
|
+
filter: Pick<IFilterRepository, "listByAccountConfig">;
|
|
20
|
+
accountSetting: Pick<IAccountSettingRepository, "get">;
|
|
21
|
+
}, accountConfigId: string, accountId: string, mailboxId: string) => Promise<MailboxBindings>;
|
|
22
|
+
/**
|
|
23
|
+
* Refuse a delete while any durable reference is bound to the folder (D16).
|
|
24
|
+
*
|
|
25
|
+
* This is what makes the settled-target rule total: a binding is only created
|
|
26
|
+
* against a settled folder and only removed by the user, so a `mailboxId` that
|
|
27
|
+
* names nothing never exists and no reader of a filter's `actionMailboxId` or a
|
|
28
|
+
* role appointment needs a missing-target branch. It removes code rather than
|
|
29
|
+
* adding a check.
|
|
30
|
+
*
|
|
31
|
+
* The alternatives both cost more than they look. `Filter` has no enabled flag
|
|
32
|
+
* and its `actionMailboxId` sentinel means "no move action", so clearing the
|
|
33
|
+
* target silently turns a move-filter into one that does something else;
|
|
34
|
+
* deleting the user's filters outright is not a decision a folder delete gets
|
|
35
|
+
* to make.
|
|
36
|
+
*
|
|
37
|
+
* The message **names** each binding rather than counting them, because a count
|
|
38
|
+
* is not actionable — and this fires on ordinary configurations, since pointing
|
|
39
|
+
* a filter at a folder is what filters are for.
|
|
40
|
+
*/
|
|
41
|
+
export declare const assertNoBindings: (mailbox: Pick<MailboxItem, "fullPath">, bindings: MailboxBindings) => void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { FILTER_NO_ACTION } from "@remit/data-ports";
|
|
2
|
+
import { BadRequestError } from "@remit/data-ports/errors";
|
|
3
|
+
import { loadFolderAppointmentsForAccount } from "./folder-role-appointments.js";
|
|
4
|
+
/**
|
|
5
|
+
* `filter.actionMailboxId` has no reverse index, so the filters are listed for
|
|
6
|
+
* the account config and matched — they are few and per-config, so this is not
|
|
7
|
+
* a mailbox scan. Role appointments are `account_setting` rows whose *value* is
|
|
8
|
+
* a mailboxId, which a `mailbox_id` column scan cannot find, so the existing
|
|
9
|
+
* per-account appointment map answers for them.
|
|
10
|
+
*/
|
|
11
|
+
export const loadMailboxBindings = async (client, accountConfigId, accountId, mailboxId) => {
|
|
12
|
+
const [filters, appointments] = await Promise.all([
|
|
13
|
+
client.filter.listByAccountConfig(accountConfigId),
|
|
14
|
+
loadFolderAppointmentsForAccount(client.accountSetting, accountConfigId, accountId),
|
|
15
|
+
]);
|
|
16
|
+
const filterNames = filters
|
|
17
|
+
.filter((filter) => filter.actionMailboxId !== FILTER_NO_ACTION &&
|
|
18
|
+
filter.actionMailboxId === mailboxId)
|
|
19
|
+
.map((filter) => filter.name);
|
|
20
|
+
const roles = [];
|
|
21
|
+
for (const [role, appointment] of appointments) {
|
|
22
|
+
if (appointment.mailboxId === mailboxId) {
|
|
23
|
+
roles.push(role);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { filterNames, roles };
|
|
27
|
+
};
|
|
28
|
+
const asList = (items) => items.length === 1
|
|
29
|
+
? items[0]
|
|
30
|
+
: `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
31
|
+
/**
|
|
32
|
+
* Refuse a delete while any durable reference is bound to the folder (D16).
|
|
33
|
+
*
|
|
34
|
+
* This is what makes the settled-target rule total: a binding is only created
|
|
35
|
+
* against a settled folder and only removed by the user, so a `mailboxId` that
|
|
36
|
+
* names nothing never exists and no reader of a filter's `actionMailboxId` or a
|
|
37
|
+
* role appointment needs a missing-target branch. It removes code rather than
|
|
38
|
+
* adding a check.
|
|
39
|
+
*
|
|
40
|
+
* The alternatives both cost more than they look. `Filter` has no enabled flag
|
|
41
|
+
* and its `actionMailboxId` sentinel means "no move action", so clearing the
|
|
42
|
+
* target silently turns a move-filter into one that does something else;
|
|
43
|
+
* deleting the user's filters outright is not a decision a folder delete gets
|
|
44
|
+
* to make.
|
|
45
|
+
*
|
|
46
|
+
* The message **names** each binding rather than counting them, because a count
|
|
47
|
+
* is not actionable — and this fires on ordinary configurations, since pointing
|
|
48
|
+
* a filter at a folder is what filters are for.
|
|
49
|
+
*/
|
|
50
|
+
export const assertNoBindings = (mailbox, bindings) => {
|
|
51
|
+
const clauses = [];
|
|
52
|
+
if (bindings.filterNames.length > 0) {
|
|
53
|
+
clauses.push(`the ${bindings.filterNames.length === 1 ? "filter" : "filters"} ${asList(bindings.filterNames.map((name) => `“${name}”`))}`);
|
|
54
|
+
}
|
|
55
|
+
if (bindings.roles.length > 0) {
|
|
56
|
+
clauses.push(`the ${asList(bindings.roles)} ${bindings.roles.length === 1 ? "role" : "roles"}`);
|
|
57
|
+
}
|
|
58
|
+
if (clauses.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
throw new BadRequestError(`“${mailbox.fullPath}” is used by ${asList(clauses)}.`);
|
|
61
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RenameMailboxInput } from "@remit/api-openapi-types";
|
|
2
|
+
import type { IAccountSettingRepository, MailboxItem } from "@remit/data-ports";
|
|
3
|
+
import { type FolderPathDeps } from "@remit/mailbox-service";
|
|
4
|
+
import type { MailboxDetailOperationIds, MailboxOperationIds, OperationHandler, TrashOperationIds } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* The mute flag and the display-name override are user preferences that live
|
|
7
|
+
* in per-mailbox AccountSetting rows (RFC 032), not on the Mailbox entity.
|
|
8
|
+
* Pick only those keys from a PATCH body: each follows the same `null` → remove,
|
|
9
|
+
* value → set, absent/undefined → no-op semantics as UpdateAddressFlagsInput.
|
|
10
|
+
* The canonical role a folder fills is appointed separately — see
|
|
11
|
+
* FolderRoleOperations.appointFolderRole (RFC 032 exclusive-folder-appointment).
|
|
12
|
+
*/
|
|
13
|
+
export declare const pickMailboxOverrideChanges: (body: RenameMailboxInput) => {
|
|
14
|
+
displayNameOverride?: string | null;
|
|
15
|
+
muted?: RenameMailboxInput["muted"];
|
|
16
|
+
};
|
|
17
|
+
export type MailboxPatchClient = FolderPathDeps & {
|
|
18
|
+
accountSetting: Pick<IAccountSettingRepository, "get" | "upsert" | "delete">;
|
|
19
|
+
};
|
|
20
|
+
export declare const applyMailboxPatch: (client: MailboxPatchClient, accountConfigId: string, mailboxId: string, accountId: string, body: RenameMailboxInput) => Promise<MailboxItem>;
|
|
21
|
+
export declare const MailboxOperations: Record<MailboxOperationIds, OperationHandler<MailboxOperationIds>>;
|
|
22
|
+
export declare const MailboxDetailOperations: Record<MailboxDetailOperationIds, OperationHandler<MailboxDetailOperationIds>>;
|
|
23
|
+
export declare const TrashOperations: Record<TrashOperationIds, OperationHandler<TrashOperationIds>>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { BadRequestError, ConflictError } from "@remit/data-ports/errors";
|
|
2
|
+
import { MessageSystemFlag } from "@remit/domain-enums";
|
|
3
|
+
import { changeFolderPath, createFolder, folderInAccount, folderInFlight, ownedAccount, ownedFolder, } from "@remit/mailbox-service";
|
|
4
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
5
|
+
import { applyPendingMoveCountPrediction, } from "../derive/pendingMoveCounts.js";
|
|
6
|
+
import { getClient } from "../service/data-client.js";
|
|
7
|
+
import { unwrap } from "../service/refusal.js";
|
|
8
|
+
import { applyMailboxOverrideChanges, loadMailboxOverrides, loadMailboxOverridesForConfig, } from "./account-overrides.js";
|
|
9
|
+
import { assertNoBindings, loadMailboxBindings } from "./mailbox-bindings.js";
|
|
10
|
+
/**
|
|
11
|
+
* The mute flag and the display-name override are user preferences that live
|
|
12
|
+
* in per-mailbox AccountSetting rows (RFC 032), not on the Mailbox entity.
|
|
13
|
+
* Pick only those keys from a PATCH body: each follows the same `null` → remove,
|
|
14
|
+
* value → set, absent/undefined → no-op semantics as UpdateAddressFlagsInput.
|
|
15
|
+
* The canonical role a folder fills is appointed separately — see
|
|
16
|
+
* FolderRoleOperations.appointFolderRole (RFC 032 exclusive-folder-appointment).
|
|
17
|
+
*/
|
|
18
|
+
export const pickMailboxOverrideChanges = (body) => {
|
|
19
|
+
const changes = {};
|
|
20
|
+
if (Object.hasOwn(body, "displayNameOverride")) {
|
|
21
|
+
changes.displayNameOverride = body.displayNameOverride;
|
|
22
|
+
}
|
|
23
|
+
if (Object.hasOwn(body, "muted")) {
|
|
24
|
+
changes.muted = body.muted;
|
|
25
|
+
}
|
|
26
|
+
return changes;
|
|
27
|
+
};
|
|
28
|
+
// Overrides land first and independently, so an override-only PATCH never
|
|
29
|
+
// touches the folder's mutation state.
|
|
30
|
+
export const applyMailboxPatch = async (client, accountConfigId, mailboxId, accountId, body) => {
|
|
31
|
+
const { fullPath } = body;
|
|
32
|
+
const applyOverrides = async () => {
|
|
33
|
+
const changes = pickMailboxOverrideChanges(body);
|
|
34
|
+
if (Object.keys(changes).length === 0)
|
|
35
|
+
return;
|
|
36
|
+
await applyMailboxOverrideChanges(client.accountSetting, accountConfigId, mailboxId, changes);
|
|
37
|
+
};
|
|
38
|
+
if (!fullPath) {
|
|
39
|
+
const folder = unwrap(await ownedFolder(client, accountConfigId, accountId, mailboxId));
|
|
40
|
+
await applyOverrides();
|
|
41
|
+
return folder;
|
|
42
|
+
}
|
|
43
|
+
const { outcome, mailbox } = unwrap(await changeFolderPath(client, accountConfigId, {
|
|
44
|
+
accountId,
|
|
45
|
+
mailboxId,
|
|
46
|
+
fullPath,
|
|
47
|
+
}));
|
|
48
|
+
await applyOverrides();
|
|
49
|
+
if (outcome === "AlreadyRecorded") {
|
|
50
|
+
throw new ConflictError(folderInFlight(mailbox));
|
|
51
|
+
}
|
|
52
|
+
return mailbox;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Every pending placement move (issue #1271) for an account, on whichever
|
|
56
|
+
* backend is active (`RemitClient.placementMove` is present on both — see
|
|
57
|
+
* `create-remit-client.ts`). Read-only; feeds
|
|
58
|
+
* `applyPendingMoveCountPrediction`'s read-time adjustment only, never mutates
|
|
59
|
+
* stored counts (epic #1281 invariant 4). Markers are written by the
|
|
60
|
+
* imap-worker bulk sync path through `RemitClient.placementMove` on every
|
|
61
|
+
* backend, so this is a real signal on the self-host stack too.
|
|
62
|
+
*/
|
|
63
|
+
const loadPendingMoves = (client, accountId) => client.placementMove.listByAccountId(accountId);
|
|
64
|
+
/**
|
|
65
|
+
* Every pending `\Seen` flag-push marker (issue #1273) for an account —
|
|
66
|
+
* `\Flagged` (star) markers are excluded, since only read/unread state feeds
|
|
67
|
+
* `unseenCount`'s prediction. `RemitClient.flagPush` is present and WRITTEN
|
|
68
|
+
* on both backends, so this is a real signal on the self-host stack too.
|
|
69
|
+
*/
|
|
70
|
+
const loadPendingUnseenFlagPushes = async (client, accountId) => {
|
|
71
|
+
const pushes = await client.flagPush.listByAccountId(accountId);
|
|
72
|
+
return pushes
|
|
73
|
+
.filter((push) => push.flagName === MessageSystemFlag.Seen)
|
|
74
|
+
.map((push) => ({ mailboxId: push.mailboxId, operation: push.operation }));
|
|
75
|
+
};
|
|
76
|
+
const toMailboxResponse = (mailbox, overrides = {}) => ({
|
|
77
|
+
mailboxId: mailbox.mailboxId,
|
|
78
|
+
accountId: mailbox.accountId,
|
|
79
|
+
namespaceType: mailbox.namespaceType,
|
|
80
|
+
namespacePrefix: mailbox.namespacePrefix,
|
|
81
|
+
hierarchyDelimiter: mailbox.hierarchyDelimiter,
|
|
82
|
+
fullPath: mailbox.fullPath,
|
|
83
|
+
messageCount: mailbox.messageCount,
|
|
84
|
+
unseenCount: mailbox.unseenCount,
|
|
85
|
+
deletedCount: mailbox.deletedCount,
|
|
86
|
+
specialUse: mailbox.specialUse ? Array.from(mailbox.specialUse) : undefined,
|
|
87
|
+
lastSyncUid: mailbox.lastSyncUid,
|
|
88
|
+
highWaterMarkUid: mailbox.highWaterMarkUid,
|
|
89
|
+
lastMessageSyncAt: mailbox.lastMessageSyncAt,
|
|
90
|
+
syncStatus: mailbox.syncStatus,
|
|
91
|
+
// The row is the only thing that knows what a rename is aiming at, and
|
|
92
|
+
// `fullPath` stays the confirmed one throughout (D2), so without this a
|
|
93
|
+
// client cannot tell a create in flight from a rename in flight, name the
|
|
94
|
+
// target of a failed rename, or offer the right retry.
|
|
95
|
+
pendingPath: mailbox.pendingPath,
|
|
96
|
+
syncFailureReason: mailbox.syncFailureReason,
|
|
97
|
+
muted: overrides.muted,
|
|
98
|
+
displayNameOverride: overrides.displayNameOverride,
|
|
99
|
+
createdAt: mailbox.createdAt,
|
|
100
|
+
updatedAt: mailbox.updatedAt,
|
|
101
|
+
});
|
|
102
|
+
export const MailboxOperations = {
|
|
103
|
+
MailboxOperations_listMailboxes: async (context, ...args) => {
|
|
104
|
+
const event = args[0];
|
|
105
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
106
|
+
const { accountId } = context.request.params;
|
|
107
|
+
const { continuationToken } = context.request.query;
|
|
108
|
+
const client = await getClient();
|
|
109
|
+
const account = await client.account.get(accountId);
|
|
110
|
+
unwrap(ownedAccount(account, accountConfigId, "read"));
|
|
111
|
+
const result = await client.mailbox.listByAccount(accountId, {
|
|
112
|
+
continuationToken,
|
|
113
|
+
});
|
|
114
|
+
// Overrides (mute / display-name / role) live in per-mailbox AccountSetting
|
|
115
|
+
// rows (RFC 032). Load the whole config's set in one query and key it by
|
|
116
|
+
// mailboxId so each mailbox surfaces its overrides without an N+1.
|
|
117
|
+
const overridesByMailbox = await loadMailboxOverridesForConfig(client.accountSetting, accountConfigId);
|
|
118
|
+
// Read-time prediction for any pending placement move (issue #1271) and
|
|
119
|
+
// pending \Seen flag push (issue #1273) — adjusts messageCount /
|
|
120
|
+
// unseenCount only, never the stored row (epic #1281 invariant 4).
|
|
121
|
+
const pendingMoves = await loadPendingMoves(client, accountId);
|
|
122
|
+
const pendingUnseenFlagPushes = await loadPendingUnseenFlagPushes(client, accountId);
|
|
123
|
+
// A folder mid-mutation is listed, labelled (D11). Hiding a `deleting` one
|
|
124
|
+
// made a delete that does not settle look like a folder that silently
|
|
125
|
+
// vanished, while the server still held it.
|
|
126
|
+
const items = applyPendingMoveCountPrediction(result.items, pendingMoves, pendingUnseenFlagPushes);
|
|
127
|
+
return {
|
|
128
|
+
items: items.map((mailbox) => toMailboxResponse(mailbox, overridesByMailbox.get(mailbox.mailboxId) ?? {})),
|
|
129
|
+
continuationToken: result.continuationToken,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
MailboxOperations_createMailbox: async (context, ...args) => {
|
|
133
|
+
const event = args[0];
|
|
134
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
135
|
+
const { accountId } = context.request.params;
|
|
136
|
+
const { namespaceType, fullPath } = context.request
|
|
137
|
+
.requestBody;
|
|
138
|
+
const created = await createFolder(await getClient(), accountConfigId, {
|
|
139
|
+
accountId,
|
|
140
|
+
namespaceType,
|
|
141
|
+
fullPath,
|
|
142
|
+
});
|
|
143
|
+
const { outcome, mailbox } = unwrap(created);
|
|
144
|
+
if (outcome === "Existing") {
|
|
145
|
+
throw new BadRequestError(`A folder named “${fullPath}” is already there.`);
|
|
146
|
+
}
|
|
147
|
+
return toMailboxResponse(mailbox);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
export const MailboxDetailOperations = {
|
|
151
|
+
MailboxDetailOperations_getMailbox: async (context, ...args) => {
|
|
152
|
+
const event = args[0];
|
|
153
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
154
|
+
const { accountId, mailboxId } = context.request.params;
|
|
155
|
+
const client = await getClient();
|
|
156
|
+
const account = await client.account.get(accountId);
|
|
157
|
+
unwrap(ownedAccount(account, accountConfigId, "read"));
|
|
158
|
+
const mailbox = await client.mailbox.get(accountId, mailboxId);
|
|
159
|
+
unwrap(folderInAccount(mailbox, accountId, "read"));
|
|
160
|
+
const overrides = await loadMailboxOverrides(client.accountSetting, accountConfigId, mailboxId);
|
|
161
|
+
// Read-time prediction for any pending placement move (issue #1271) and
|
|
162
|
+
// pending \Seen flag push (issue #1273) — adjusts messageCount /
|
|
163
|
+
// unseenCount only, never the stored row (epic #1281 invariant 4).
|
|
164
|
+
const pendingMoves = await loadPendingMoves(client, accountId);
|
|
165
|
+
const pendingUnseenFlagPushes = await loadPendingUnseenFlagPushes(client, accountId);
|
|
166
|
+
const adjusted = applyPendingMoveCountPrediction([mailbox], pendingMoves, pendingUnseenFlagPushes)[0] ?? mailbox;
|
|
167
|
+
return toMailboxResponse(adjusted, overrides);
|
|
168
|
+
},
|
|
169
|
+
MailboxDetailOperations_renameMailbox: async (context, ...args) => {
|
|
170
|
+
const event = args[0];
|
|
171
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
172
|
+
const { accountId, mailboxId } = context.request.params;
|
|
173
|
+
const body = context.request.requestBody;
|
|
174
|
+
const client = await getClient();
|
|
175
|
+
const mailbox = await applyMailboxPatch(client, accountConfigId, mailboxId, accountId, body);
|
|
176
|
+
const overrides = await loadMailboxOverrides(client.accountSetting, accountConfigId, mailboxId);
|
|
177
|
+
return toMailboxResponse(mailbox, overrides);
|
|
178
|
+
},
|
|
179
|
+
MailboxDetailOperations_deleteMailbox: async (context, ...args) => {
|
|
180
|
+
const event = args[0];
|
|
181
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
182
|
+
const { accountId, mailboxId } = context.request.params;
|
|
183
|
+
const client = await getClient();
|
|
184
|
+
const account = await client.account.get(accountId);
|
|
185
|
+
unwrap(ownedAccount(account, accountConfigId, "act"));
|
|
186
|
+
const mailbox = await client.mailbox.get(accountId, mailboxId);
|
|
187
|
+
unwrap(folderInAccount(mailbox, accountId, "act"));
|
|
188
|
+
// Cheapest first (D4, D13). None of the three becomes possible by
|
|
189
|
+
// retrying the same request, so all three are 400 and all three run
|
|
190
|
+
// before anything is recorded.
|
|
191
|
+
if (mailbox.fullPath.toUpperCase() === "INBOX") {
|
|
192
|
+
throw new BadRequestError("The inbox can't be deleted.");
|
|
193
|
+
}
|
|
194
|
+
const children = await client.mailbox.findByPathPrefix(accountId, mailbox.fullPath, mailbox.hierarchyDelimiter);
|
|
195
|
+
if (children.length > 0) {
|
|
196
|
+
// The server would keep the parent's name as a placeholder and leave the
|
|
197
|
+
// children untouched — an outcome no local row describes (D9).
|
|
198
|
+
throw new BadRequestError(`“${mailbox.fullPath}” has folders inside it. Delete those first.`);
|
|
199
|
+
}
|
|
200
|
+
assertNoBindings(mailbox, await loadMailboxBindings(client, accountConfigId, accountId, mailboxId));
|
|
201
|
+
await client.mailboxQueue.deleteMailbox(mailboxId, accountId);
|
|
202
|
+
return { statusCode: 204 };
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
export const TrashOperations = {
|
|
206
|
+
TrashOperations_emptyTrash: async (context, ...args) => {
|
|
207
|
+
const event = args[0];
|
|
208
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
209
|
+
const { accountId } = context.request.params;
|
|
210
|
+
const client = await getClient();
|
|
211
|
+
const account = await client.account.get(accountId);
|
|
212
|
+
unwrap(ownedAccount(account, accountConfigId, "act"));
|
|
213
|
+
// The service resolves Trash, marks the rows and counts them, all off one
|
|
214
|
+
// read, and refuses with a coded 409 when the role is unresolved. Counting
|
|
215
|
+
// here as well would be a second answer free to disagree with the one that
|
|
216
|
+
// acts, and "0 deleted" reads as success to a user whose Trash is untouched.
|
|
217
|
+
return client.messageMove.emptyTrash(accountConfigId, accountId);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { BadRequestError, NotFoundError } from "@remit/data-ports/errors";
|
|
3
|
+
import { SYNC_WARNING_SURFACE_ATTEMPTS } from "@remit/data-ports/sync-warning-state";
|
|
4
|
+
import { logger } from "@remit/logger";
|
|
5
|
+
import { env } from "expect-env";
|
|
6
|
+
import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
|
|
7
|
+
import { getClient } from "../service/data-client.js";
|
|
8
|
+
import { sqsClient } from "../service/sqs.js";
|
|
9
|
+
import { toVipSuggestionEntry } from "./vip-suggestions.js";
|
|
10
|
+
const DOWNLOAD_URL_TTL_SECONDS = 60 * 60;
|
|
11
|
+
const SURFACED_WARNING_CAP = 200;
|
|
12
|
+
export const MeOperations = {
|
|
13
|
+
MeOperations_listVipSuggestions: async (_context, ...args) => {
|
|
14
|
+
const event = args[0];
|
|
15
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
16
|
+
const { address } = await getClient();
|
|
17
|
+
const items = await address.listSuggestedVips({ accountConfigId });
|
|
18
|
+
return { suggestions: items.map(toVipSuggestionEntry) };
|
|
19
|
+
},
|
|
20
|
+
MeOperations_listQuarantine: async (_context, ...args) => {
|
|
21
|
+
const event = args[0];
|
|
22
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
23
|
+
const { quarantine, syncWarning } = await getClient();
|
|
24
|
+
const [entries, warnings] = await Promise.all([
|
|
25
|
+
quarantine.listByAccountConfigId(accountConfigId),
|
|
26
|
+
syncWarning.listSurfacedByAccountConfigId(accountConfigId, SYNC_WARNING_SURFACE_ATTEMPTS, SURFACED_WARNING_CAP),
|
|
27
|
+
]);
|
|
28
|
+
return {
|
|
29
|
+
entries,
|
|
30
|
+
warnings: warnings.items,
|
|
31
|
+
warningsTotal: warnings.total,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
MeOperations_deleteMe: async (_context, ...args) => {
|
|
35
|
+
const event = args[0];
|
|
36
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
37
|
+
const { confirmEmail } = JSON.parse(event.body ?? "{}");
|
|
38
|
+
const callerEmail = event.requestContext?.authorizer?.claims?.email;
|
|
39
|
+
if (typeof confirmEmail !== "string" || confirmEmail.trim() === "") {
|
|
40
|
+
throw new BadRequestError("confirmEmail is required");
|
|
41
|
+
}
|
|
42
|
+
if (!callerEmail) {
|
|
43
|
+
throw new BadRequestError("Account email claim missing from token");
|
|
44
|
+
}
|
|
45
|
+
if (confirmEmail.toLowerCase() !== callerEmail.toLowerCase()) {
|
|
46
|
+
throw new BadRequestError("confirmEmail does not match your account email");
|
|
47
|
+
}
|
|
48
|
+
const { accountConfig } = await getClient();
|
|
49
|
+
try {
|
|
50
|
+
await accountConfig.update(accountConfigId, {
|
|
51
|
+
state: "deleting",
|
|
52
|
+
deletedAt: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// If already in "deleting" state, treat as idempotent
|
|
57
|
+
if (err instanceof Error &&
|
|
58
|
+
(err.name === "ConditionalCheckFailedException" ||
|
|
59
|
+
err.message.includes("ConditionalCheckFailed"))) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: 202,
|
|
62
|
+
message: "Account deletion already in progress",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
const queueUrl = env.SQS_QUEUE_URL_ACCOUNT_FANOUT;
|
|
68
|
+
await sqsClient.send(new SendMessageCommand({
|
|
69
|
+
QueueUrl: queueUrl,
|
|
70
|
+
MessageBody: JSON.stringify({
|
|
71
|
+
type: "AccountDelete",
|
|
72
|
+
accountConfigId,
|
|
73
|
+
}),
|
|
74
|
+
}));
|
|
75
|
+
// biome-ignore lint/plugin/no-logger-info: account deletion is an audit-grade signal
|
|
76
|
+
logger.info({ accountConfigId }, "Account deletion initiated");
|
|
77
|
+
return {
|
|
78
|
+
statusCode: 202,
|
|
79
|
+
message: "Account deletion initiated",
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
MeOperations_createExport: async (_context, ...args) => {
|
|
83
|
+
const event = args[0];
|
|
84
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
85
|
+
const userId = getSubFromEvent(event);
|
|
86
|
+
if (!userId) {
|
|
87
|
+
throw new Error("Missing Cognito `sub`: cannot attribute an export request to a user");
|
|
88
|
+
}
|
|
89
|
+
const { accountExportRequest } = await getClient();
|
|
90
|
+
const exportRequest = await accountExportRequest.create({
|
|
91
|
+
accountConfigId,
|
|
92
|
+
userId,
|
|
93
|
+
state: "Pending",
|
|
94
|
+
});
|
|
95
|
+
await sqsClient.send(new SendMessageCommand({
|
|
96
|
+
QueueUrl: env.SQS_QUEUE_URL_ACCOUNT_FANOUT,
|
|
97
|
+
MessageBody: JSON.stringify({
|
|
98
|
+
type: "AccountExport",
|
|
99
|
+
accountConfigId,
|
|
100
|
+
accountExportRequestId: exportRequest.accountExportRequestId,
|
|
101
|
+
}),
|
|
102
|
+
}));
|
|
103
|
+
// biome-ignore lint/plugin/no-logger-info: account export is an audit-grade signal
|
|
104
|
+
logger.info({
|
|
105
|
+
accountConfigId,
|
|
106
|
+
accountExportRequestId: exportRequest.accountExportRequestId,
|
|
107
|
+
}, "Account export initiated");
|
|
108
|
+
return {
|
|
109
|
+
statusCode: 202,
|
|
110
|
+
exportId: exportRequest.accountExportRequestId,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
MeOperations_getExport: async (context, ...args) => {
|
|
114
|
+
const event = args[0];
|
|
115
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
116
|
+
const exportId = context.request.params.exportId;
|
|
117
|
+
const { accountExportRequest, storage } = await getClient();
|
|
118
|
+
const exportRequest = await accountExportRequest.get(exportId);
|
|
119
|
+
if (exportRequest.accountConfigId !== accountConfigId) {
|
|
120
|
+
throw new NotFoundError(`Export not found: ${exportId}`);
|
|
121
|
+
}
|
|
122
|
+
const downloadUrl = exportRequest.state === "Ready" && exportRequest.objectKey
|
|
123
|
+
? await storage.getPresignedDownloadUrl(exportRequest.objectKey, DOWNLOAD_URL_TTL_SECONDS)
|
|
124
|
+
: undefined;
|
|
125
|
+
return {
|
|
126
|
+
accountExportRequestId: exportRequest.accountExportRequestId,
|
|
127
|
+
accountConfigId: exportRequest.accountConfigId,
|
|
128
|
+
userId: exportRequest.userId,
|
|
129
|
+
state: exportRequest.state,
|
|
130
|
+
createdAt: exportRequest.createdAt,
|
|
131
|
+
updatedAt: exportRequest.updatedAt,
|
|
132
|
+
objectKey: exportRequest.objectKey,
|
|
133
|
+
downloadUrl,
|
|
134
|
+
expiresAt: exportRequest.expiresAt,
|
|
135
|
+
errorMessage: exportRequest.errorMessage,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|