@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.
Files changed (269) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -522
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. package/tsconfig.json +0 -7
@@ -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
- };