@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
package/src/handlers/filter.ts
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CreateFilterInput,
|
|
3
|
-
CreateFilterResponse,
|
|
4
|
-
FilterResponse,
|
|
5
|
-
UpdateFilterInput as UpdateFilterRequestBody,
|
|
6
|
-
} from "@remit/api-openapi-types";
|
|
7
|
-
import {
|
|
8
|
-
deriveFilterTtl,
|
|
9
|
-
FILTER_NO_ACTION,
|
|
10
|
-
type FilterItem,
|
|
11
|
-
type IFilterAnchorTransaction,
|
|
12
|
-
type IMailboxRepository,
|
|
13
|
-
type UpdateFilterInput,
|
|
14
|
-
} from "@remit/data-ports";
|
|
15
|
-
import { BadRequestError } from "@remit/data-ports/errors";
|
|
16
|
-
import {
|
|
17
|
-
FilterDisabledReason,
|
|
18
|
-
FilterScope,
|
|
19
|
-
FilterState,
|
|
20
|
-
} from "@remit/domain-enums";
|
|
21
|
-
import type { AnchorPayload } from "@remit/search-service";
|
|
22
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
23
|
-
import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
|
|
24
|
-
import { getClient } from "../service/data-client.js";
|
|
25
|
-
import { buildFilterAnchor } from "../service/filter.js";
|
|
26
|
-
import { queueOrganizeJob } from "../service/organize-queue.js";
|
|
27
|
-
import type {
|
|
28
|
-
FilterDetailOperationIds,
|
|
29
|
-
FilterOperationIds,
|
|
30
|
-
OperationHandler,
|
|
31
|
-
} from "../types.js";
|
|
32
|
-
import { assertAccountOwnership } from "./account-ownership.js";
|
|
33
|
-
import { assertMailboxInAccount, assertMailboxSettled } from "./mailbox.js";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Minimal filter-service surface the create handler needs — declared as a
|
|
37
|
-
* `Pick` so tests can stub it without a live table.
|
|
38
|
-
*/
|
|
39
|
-
export interface FilterCrudDeps {
|
|
40
|
-
/**
|
|
41
|
-
* Creates the `Filter` row and its optional sibling `FilterAnchor` row in
|
|
42
|
-
* one transaction (#351) — a failure on the anchor write can never leave a
|
|
43
|
-
* `Filter` durably marked `hasAnchor: true` with no matching anchor row,
|
|
44
|
-
* and it surfaces as a failed create request rather than a silently
|
|
45
|
-
* broken filter.
|
|
46
|
-
*/
|
|
47
|
-
filterAnchorTransaction: IFilterAnchorTransaction;
|
|
48
|
-
buildAnchor(
|
|
49
|
-
accountConfigId: string,
|
|
50
|
-
anchorMessageId: string,
|
|
51
|
-
): Promise<AnchorPayload | null>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Reduce a PATCH body to the fields a filter update may set (RFC 034, reader
|
|
56
|
-
* #266). Preserves absence: a key not present in the body is not present in
|
|
57
|
-
* the patch, so a name-only rename yields `{ name }` and never touches a
|
|
58
|
-
* predicate/action/scope/expiresAt field — the service's
|
|
59
|
-
* `changesRuleAssertion` guard then leaves `ruleChangedAt` untouched (Decision
|
|
60
|
-
* 3.2). Any field outside this set — most notably a server-derived
|
|
61
|
-
* `state`/`ttl`/`hasAnchor`/`ruleChangedAt` smuggled into the body — is
|
|
62
|
-
* dropped. `scope`/`expiresAt` land here as the caller sent them; a patch that
|
|
63
|
-
* touches either still needs `resolveFilterScopeExpiry` to merge them against
|
|
64
|
-
* the stored row and derive `ttl`/`state` before this reaches the repo.
|
|
65
|
-
*/
|
|
66
|
-
export const pickFilterUpdate = (
|
|
67
|
-
body: Partial<UpdateFilterRequestBody>,
|
|
68
|
-
): Partial<UpdateFilterInput> => {
|
|
69
|
-
const patch: Partial<UpdateFilterInput> = {};
|
|
70
|
-
if (Object.hasOwn(body, "name")) patch.name = body.name;
|
|
71
|
-
if (Object.hasOwn(body, "scope")) patch.scope = body.scope;
|
|
72
|
-
if (Object.hasOwn(body, "expiresAt")) patch.expiresAt = body.expiresAt;
|
|
73
|
-
if (Object.hasOwn(body, "matchOperator")) {
|
|
74
|
-
patch.matchOperator = body.matchOperator;
|
|
75
|
-
}
|
|
76
|
-
if (Object.hasOwn(body, "literalClauses")) {
|
|
77
|
-
patch.literalClauses = body.literalClauses;
|
|
78
|
-
}
|
|
79
|
-
if (Object.hasOwn(body, "actionLabelId")) {
|
|
80
|
-
patch.actionLabelId = body.actionLabelId;
|
|
81
|
-
}
|
|
82
|
-
if (Object.hasOwn(body, "actionMailboxId")) {
|
|
83
|
-
patch.actionMailboxId = body.actionMailboxId;
|
|
84
|
-
}
|
|
85
|
-
if (Object.hasOwn(body, "state")) patch.state = body.state;
|
|
86
|
-
return patch;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* A filter's semantic anchor is set once, from `anchorMessageId`, only on
|
|
91
|
-
* `CreateFilterInput` (RFC 034 Decision 2) — the generated `UpdateFilterInput`
|
|
92
|
-
* carries no anchor field at all, so a well-behaved client cannot express this.
|
|
93
|
-
* This only catches a caller handing the raw PATCH body an anchor field
|
|
94
|
-
* anyway, and rejects it loudly rather than silently dropping it (reader
|
|
95
|
-
* #266): repointing an anchor would silently change what a saved filter
|
|
96
|
-
* matches with nothing visible changing, and that deserves a new filter
|
|
97
|
-
* instead — scope and expiry cover the rest of what changed here.
|
|
98
|
-
*/
|
|
99
|
-
const ANCHOR_MUTATION_FIELDS = ["anchorMessageId"] as const;
|
|
100
|
-
|
|
101
|
-
export const rejectAnchorMutation = (body: Record<string, unknown>): void => {
|
|
102
|
-
const attempted = ANCHOR_MUTATION_FIELDS.find((field) =>
|
|
103
|
-
Object.hasOwn(body, field),
|
|
104
|
-
);
|
|
105
|
-
if (!attempted) return;
|
|
106
|
-
throw new BadRequestError(
|
|
107
|
-
"A filter's semantic anchor can't change after creation — create a new filter instead.",
|
|
108
|
-
);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export interface ResolvedScopeExpiry {
|
|
112
|
-
scope: FilterItem["scope"];
|
|
113
|
-
expiresAt?: string;
|
|
114
|
-
ttl?: number;
|
|
115
|
-
state: FilterItem["state"];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Merge a PATCH body's `scope`/`expiresAt` against the filter's stored values
|
|
120
|
-
* and resolve the full set the row must land on (reader #266). Only called
|
|
121
|
-
* when the patch touches `scope` or `expiresAt` — one that touches neither
|
|
122
|
-
* leaves both, and `ttl`/`state`, untouched exactly as before this ticket.
|
|
123
|
-
*
|
|
124
|
-
* A `Temporary` result always carries a defined `expiresAt` — moving to
|
|
125
|
-
* `Temporary` without one is rejected, matching the Filter model's invariant
|
|
126
|
-
* that a Temporary filter always has an expiry (RFC 034 Decision 1.1). Moving
|
|
127
|
-
* to `Standing` clears `expiresAt`/`ttl`, matching the model's invariant that
|
|
128
|
-
* both stay absent outside `Temporary` (RFC 034 Decision 1.4) — the reserved
|
|
129
|
-
* `ttl` attribute must never linger once a filter is no longer temporary.
|
|
130
|
-
*
|
|
131
|
-
* `state` is recomputed the same comparison `refreshExpiry` runs, so a filter
|
|
132
|
-
* extended past a lapsed `expiresAt` (or switched to `Standing`) reads Active
|
|
133
|
-
* immediately — the index-time worker's `byAccountAndState` query needs that
|
|
134
|
-
* now, not on whatever read happens to touch the row next.
|
|
135
|
-
*/
|
|
136
|
-
export const resolveFilterScopeExpiry = (
|
|
137
|
-
current: Pick<FilterItem, "scope" | "expiresAt">,
|
|
138
|
-
patch: Partial<UpdateFilterInput>,
|
|
139
|
-
): ResolvedScopeExpiry => {
|
|
140
|
-
const scope = patch.scope ?? current.scope;
|
|
141
|
-
|
|
142
|
-
if (scope !== FilterScope.Temporary) {
|
|
143
|
-
if (Object.hasOwn(patch, "expiresAt") && patch.expiresAt) {
|
|
144
|
-
throw new BadRequestError(
|
|
145
|
-
"expiresAt only applies to a Temporary filter — switch scope to Temporary to set one.",
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
scope,
|
|
150
|
-
expiresAt: undefined,
|
|
151
|
-
ttl: undefined,
|
|
152
|
-
state: FilterState.Active,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const expiresAt = Object.hasOwn(patch, "expiresAt")
|
|
157
|
-
? patch.expiresAt
|
|
158
|
-
: current.expiresAt;
|
|
159
|
-
if (!expiresAt) {
|
|
160
|
-
throw new BadRequestError(
|
|
161
|
-
"A Temporary filter needs expiresAt — pick a date, or switch scope to Standing.",
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const ttl = deriveFilterTtl(scope, expiresAt);
|
|
166
|
-
const state =
|
|
167
|
-
new Date(expiresAt).getTime() > Date.now()
|
|
168
|
-
? FilterState.Active
|
|
169
|
-
: FilterState.Expired;
|
|
170
|
-
|
|
171
|
-
return { scope, expiresAt, ttl, state };
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const hasLapsed = (
|
|
175
|
-
filter: Pick<FilterItem, "scope" | "expiresAt">,
|
|
176
|
-
now: number,
|
|
177
|
-
): boolean =>
|
|
178
|
-
filter.scope === FilterScope.Temporary &&
|
|
179
|
-
filter.expiresAt !== undefined &&
|
|
180
|
-
new Date(filter.expiresAt).getTime() <= now;
|
|
181
|
-
|
|
182
|
-
const FOLDERLESS_REASONS: ReadonlySet<FilterItem["disabledReason"]> = new Set([
|
|
183
|
-
FilterDisabledReason.AwaitingFolder,
|
|
184
|
-
FilterDisabledReason.FolderCreateFailed,
|
|
185
|
-
]);
|
|
186
|
-
|
|
187
|
-
const folderlessRefusal = (
|
|
188
|
-
reason: FilterItem["disabledReason"],
|
|
189
|
-
): BadRequestError =>
|
|
190
|
-
new BadRequestError(
|
|
191
|
-
reason === FilterDisabledReason.AwaitingFolder
|
|
192
|
-
? "This filter has no folder to move mail into yet. Wait for its folder to be created on the mail server, or pick a folder in the rule, then turn it on."
|
|
193
|
-
: "The mail server refused to create this filter's folder. Pick a folder in the rule, then turn it on.",
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
export const resolveFilterUpdate = (
|
|
197
|
-
current: Pick<
|
|
198
|
-
FilterItem,
|
|
199
|
-
"scope" | "expiresAt" | "state" | "disabledReason" | "actionMailboxId"
|
|
200
|
-
>,
|
|
201
|
-
patch: Partial<UpdateFilterInput>,
|
|
202
|
-
now: number = Date.now(),
|
|
203
|
-
): Partial<UpdateFilterInput> => {
|
|
204
|
-
if (patch.state === FilterState.Expired) {
|
|
205
|
-
throw new BadRequestError(
|
|
206
|
-
"A filter expires through its date, not its state. Set it Active or Disabled, or change its expiry.",
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
const folderless =
|
|
210
|
-
current.state === FilterState.Disabled &&
|
|
211
|
-
FOLDERLESS_REASONS.has(current.disabledReason);
|
|
212
|
-
const folder = patch.actionMailboxId ?? current.actionMailboxId;
|
|
213
|
-
const picked = folderless && folder !== FILTER_NO_ACTION;
|
|
214
|
-
const pickedReason = picked
|
|
215
|
-
? { disabledReason: FilterDisabledReason.UserDisabled }
|
|
216
|
-
: {};
|
|
217
|
-
|
|
218
|
-
const touchesTiming =
|
|
219
|
-
Object.hasOwn(patch, "scope") || Object.hasOwn(patch, "expiresAt");
|
|
220
|
-
if (!touchesTiming && patch.state === undefined) {
|
|
221
|
-
return { ...patch, ...pickedReason };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const timing = touchesTiming
|
|
225
|
-
? resolveFilterScopeExpiry(current, patch)
|
|
226
|
-
: undefined;
|
|
227
|
-
const lapsed = timing
|
|
228
|
-
? timing.state === FilterState.Expired
|
|
229
|
-
: hasLapsed(current, now);
|
|
230
|
-
|
|
231
|
-
if (patch.state === FilterState.Active && lapsed) {
|
|
232
|
-
throw new BadRequestError(
|
|
233
|
-
"This filter has expired. Move its date forward or make it Standing to turn it back on.",
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const wanted = patch.state ?? current.state;
|
|
238
|
-
const merged = { ...patch, ...timing };
|
|
239
|
-
if (wanted === FilterState.Disabled) {
|
|
240
|
-
return {
|
|
241
|
-
...merged,
|
|
242
|
-
state: FilterState.Disabled,
|
|
243
|
-
...(folderless
|
|
244
|
-
? pickedReason
|
|
245
|
-
: patch.state === FilterState.Disabled
|
|
246
|
-
? { disabledReason: FilterDisabledReason.UserDisabled }
|
|
247
|
-
: {}),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
if (lapsed) {
|
|
251
|
-
return {
|
|
252
|
-
...merged,
|
|
253
|
-
state: FilterState.Expired,
|
|
254
|
-
disabledReason: FilterDisabledReason.None,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
if (folderless && folder === FILTER_NO_ACTION) {
|
|
258
|
-
throw folderlessRefusal(current.disabledReason);
|
|
259
|
-
}
|
|
260
|
-
return {
|
|
261
|
-
...merged,
|
|
262
|
-
state: FilterState.Active,
|
|
263
|
-
disabledReason: FilterDisabledReason.None,
|
|
264
|
-
};
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const assertEnabledTargetExists = async (
|
|
268
|
-
client: {
|
|
269
|
-
mailbox: Pick<IMailboxRepository, "get" | "resolveAccountId">;
|
|
270
|
-
},
|
|
271
|
-
accountId: string,
|
|
272
|
-
actionMailboxId: string,
|
|
273
|
-
): Promise<void> => {
|
|
274
|
-
if (actionMailboxId === FILTER_NO_ACTION) return;
|
|
275
|
-
if ((await client.mailbox.resolveAccountId(actionMailboxId)) === null) {
|
|
276
|
-
throw new BadRequestError(
|
|
277
|
-
"The folder this filter moves mail into no longer exists. Pick another folder, then turn the filter on.",
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
await assertActionMailboxSettled(client, accountId, actionMailboxId);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const toFilterResponse = (item: FilterItem): FilterResponse => ({
|
|
284
|
-
filterId: item.filterId,
|
|
285
|
-
accountConfigId: item.accountConfigId,
|
|
286
|
-
name: item.name,
|
|
287
|
-
scope: item.scope,
|
|
288
|
-
expiresAt: item.expiresAt,
|
|
289
|
-
state: item.state,
|
|
290
|
-
disabledReason: item.disabledReason,
|
|
291
|
-
hasAnchor: item.hasAnchor,
|
|
292
|
-
ruleChangedAt: item.ruleChangedAt,
|
|
293
|
-
actionChangedAt: item.actionChangedAt,
|
|
294
|
-
matchOperator: item.matchOperator,
|
|
295
|
-
literalClauses: item.literalClauses,
|
|
296
|
-
actionLabelId: item.actionLabelId,
|
|
297
|
-
actionMailboxId: item.actionMailboxId,
|
|
298
|
-
createdAt: item.createdAt,
|
|
299
|
-
updatedAt: item.updatedAt,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Create a filter, wiring the semantic anchor when `anchorMessageId` is set
|
|
304
|
-
* (RFC 034 Decision 2). The anchor is built first, then the `Filter` row and
|
|
305
|
-
* its sibling `FilterAnchor` row are written together in one transaction
|
|
306
|
-
* (#351) — never as two independent writes a partial failure could split. A
|
|
307
|
-
* message with no indexed chunks yields no anchor: the filter is created as
|
|
308
|
-
* purely literal (`hasAnchor: false`) rather than with an empty anchor.
|
|
309
|
-
*/
|
|
310
|
-
export const createFilterWithAnchor = async (
|
|
311
|
-
deps: FilterCrudDeps,
|
|
312
|
-
accountConfigId: string,
|
|
313
|
-
input: CreateFilterInput,
|
|
314
|
-
): Promise<FilterItem> => {
|
|
315
|
-
const { anchorMessageId } = input;
|
|
316
|
-
const anchor = anchorMessageId
|
|
317
|
-
? await deps.buildAnchor(accountConfigId, anchorMessageId)
|
|
318
|
-
: null;
|
|
319
|
-
|
|
320
|
-
return deps.filterAnchorTransaction.createWithAnchor(
|
|
321
|
-
{
|
|
322
|
-
accountConfigId,
|
|
323
|
-
name: input.name,
|
|
324
|
-
scope: input.scope,
|
|
325
|
-
expiresAt: input.expiresAt,
|
|
326
|
-
ttl: deriveFilterTtl(input.scope, input.expiresAt),
|
|
327
|
-
matchOperator: input.matchOperator,
|
|
328
|
-
literalClauses: input.literalClauses,
|
|
329
|
-
actionLabelId: input.actionLabelId,
|
|
330
|
-
actionMailboxId: input.actionMailboxId,
|
|
331
|
-
hasAnchor: anchor !== null,
|
|
332
|
-
},
|
|
333
|
-
anchor && anchorMessageId
|
|
334
|
-
? {
|
|
335
|
-
accountConfigId,
|
|
336
|
-
anchorMessageId,
|
|
337
|
-
anchorEmbedding: anchor.anchorEmbedding,
|
|
338
|
-
anchorEmbeddingId: anchor.anchorEmbeddingId,
|
|
339
|
-
anchorSourceText: anchor.anchorSourceText,
|
|
340
|
-
}
|
|
341
|
-
: null,
|
|
342
|
-
);
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const requireUserId = (event: APIGatewayProxyEvent): string => {
|
|
346
|
-
const userId = getSubFromEvent(event);
|
|
347
|
-
if (!userId) {
|
|
348
|
-
throw new Error(
|
|
349
|
-
"Missing Cognito `sub`: cannot attribute an organize job to a user",
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
return userId;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* A filter's `actionMailboxId` is a durable reference, so its target has to be
|
|
357
|
-
* a folder the mail server has settled (D12, first row) — 422 otherwise. The
|
|
358
|
-
* sentinel means "no move action" and binds to nothing, so it is not gated,
|
|
359
|
-
* and neither is a filter that touches the field at all.
|
|
360
|
-
*/
|
|
361
|
-
const assertActionMailboxSettled = async (
|
|
362
|
-
client: { mailbox: Pick<IMailboxRepository, "get"> },
|
|
363
|
-
accountId: string,
|
|
364
|
-
actionMailboxId: string | undefined,
|
|
365
|
-
): Promise<void> => {
|
|
366
|
-
if (actionMailboxId === undefined || actionMailboxId === FILTER_NO_ACTION) {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const target = await client.mailbox.get(accountId, actionMailboxId);
|
|
370
|
-
assertMailboxInAccount(target, accountId, "act");
|
|
371
|
-
assertMailboxSettled(target);
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
export const FilterOperations: Record<
|
|
375
|
-
FilterOperationIds,
|
|
376
|
-
OperationHandler<FilterOperationIds>
|
|
377
|
-
> = {
|
|
378
|
-
FilterOperations_listFilters: async (context, ...args: unknown[]) => {
|
|
379
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
380
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
381
|
-
const { accountId } = context.request.params as { accountId: string };
|
|
382
|
-
const { continuationToken } = context.request.query as {
|
|
383
|
-
continuationToken?: string;
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const client = await getClient();
|
|
387
|
-
const account = await client.account.get(accountId);
|
|
388
|
-
assertAccountOwnership(account, accountConfigId, "read");
|
|
389
|
-
|
|
390
|
-
const { filter } = client;
|
|
391
|
-
const page = await filter.listPageByAccountConfig(accountConfigId, {
|
|
392
|
-
continuationToken,
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
// Reading the list is one of the touch points that lazily expires a past
|
|
396
|
-
// Temporary filter (RFC 034 Decision 1.2) — a no-op write for Standing or
|
|
397
|
-
// not-yet-expired filters.
|
|
398
|
-
const items = await Promise.all(
|
|
399
|
-
page.items.map((item) => filter.refreshExpiry(item)),
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
return {
|
|
403
|
-
items: items.map(toFilterResponse),
|
|
404
|
-
continuationToken: page.continuationToken,
|
|
405
|
-
};
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
FilterOperations_createFilter: async (
|
|
409
|
-
context,
|
|
410
|
-
...args: unknown[]
|
|
411
|
-
): Promise<CreateFilterResponse> => {
|
|
412
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
413
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
414
|
-
const { accountId } = context.request.params as { accountId: string };
|
|
415
|
-
const input = context.request.requestBody as CreateFilterInput;
|
|
416
|
-
const userId = requireUserId(event);
|
|
417
|
-
|
|
418
|
-
const client = await getClient();
|
|
419
|
-
const account = await client.account.get(accountId);
|
|
420
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
421
|
-
await assertActionMailboxSettled(client, accountId, input.actionMailboxId);
|
|
422
|
-
|
|
423
|
-
const filter = await createFilterWithAnchor(
|
|
424
|
-
{
|
|
425
|
-
filterAnchorTransaction: client.filterAnchorTransaction,
|
|
426
|
-
buildAnchor: buildFilterAnchor,
|
|
427
|
-
},
|
|
428
|
-
accountConfigId,
|
|
429
|
-
input,
|
|
430
|
-
);
|
|
431
|
-
// The new filter's first pass over the inbox. The worker reads the filter
|
|
432
|
-
// when it runs and refuses one that has nothing to run (#1354).
|
|
433
|
-
const job = await queueOrganizeJob(client, {
|
|
434
|
-
accountConfigId,
|
|
435
|
-
userId,
|
|
436
|
-
filterId: filter.filterId,
|
|
437
|
-
});
|
|
438
|
-
return { ...toFilterResponse(filter), organizeJobId: job.organizeJobId };
|
|
439
|
-
},
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
export const FilterDetailOperations: Record<
|
|
443
|
-
FilterDetailOperationIds,
|
|
444
|
-
OperationHandler<FilterDetailOperationIds>
|
|
445
|
-
> = {
|
|
446
|
-
FilterDetailOperations_getFilter: async (context, ...args: unknown[]) => {
|
|
447
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
448
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
449
|
-
const { accountId, filterId } = context.request.params as {
|
|
450
|
-
accountId: string;
|
|
451
|
-
filterId: string;
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
const client = await getClient();
|
|
455
|
-
const account = await client.account.get(accountId);
|
|
456
|
-
assertAccountOwnership(account, accountConfigId, "read");
|
|
457
|
-
|
|
458
|
-
const { filter } = client;
|
|
459
|
-
const item = await filter.get(accountConfigId, filterId);
|
|
460
|
-
const refreshed = await filter.refreshExpiry(item);
|
|
461
|
-
return toFilterResponse(refreshed);
|
|
462
|
-
},
|
|
463
|
-
|
|
464
|
-
FilterDetailOperations_updateFilter: async (context, ...args: unknown[]) => {
|
|
465
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
466
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
467
|
-
const { accountId, filterId } = context.request.params as {
|
|
468
|
-
accountId: string;
|
|
469
|
-
filterId: string;
|
|
470
|
-
};
|
|
471
|
-
const body = context.request.requestBody as Record<string, unknown>;
|
|
472
|
-
rejectAnchorMutation(body);
|
|
473
|
-
|
|
474
|
-
const client = await getClient();
|
|
475
|
-
const account = await client.account.get(accountId);
|
|
476
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
477
|
-
|
|
478
|
-
const { filter } = client;
|
|
479
|
-
const patch = pickFilterUpdate(body as Partial<UpdateFilterRequestBody>);
|
|
480
|
-
await assertActionMailboxSettled(client, accountId, patch.actionMailboxId);
|
|
481
|
-
const current = await filter.get(accountConfigId, filterId);
|
|
482
|
-
const resolvedPatch = resolveFilterUpdate(current, patch);
|
|
483
|
-
if (
|
|
484
|
-
patch.state === FilterState.Active &&
|
|
485
|
-
patch.actionMailboxId === undefined
|
|
486
|
-
) {
|
|
487
|
-
await assertEnabledTargetExists(
|
|
488
|
-
client,
|
|
489
|
-
accountId,
|
|
490
|
-
current.actionMailboxId,
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const updated = await filter.update(
|
|
495
|
-
accountConfigId,
|
|
496
|
-
filterId,
|
|
497
|
-
resolvedPatch,
|
|
498
|
-
);
|
|
499
|
-
return toFilterResponse(updated);
|
|
500
|
-
},
|
|
501
|
-
|
|
502
|
-
FilterDetailOperations_deleteFilter: async (context, ...args: unknown[]) => {
|
|
503
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
504
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
505
|
-
const { accountId, filterId } = context.request.params as {
|
|
506
|
-
accountId: string;
|
|
507
|
-
filterId: string;
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const client = await getClient();
|
|
511
|
-
const account = await client.account.get(accountId);
|
|
512
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
513
|
-
|
|
514
|
-
const { filter } = client;
|
|
515
|
-
await filter.delete(accountConfigId, filterId);
|
|
516
|
-
return { statusCode: 204 };
|
|
517
|
-
},
|
|
518
|
-
};
|