@remit/imap-worker 0.0.90 → 0.0.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/account-check.d.ts +27 -0
  2. package/dist/account-check.js +70 -0
  3. package/dist/body-sync-gate.d.ts +12 -0
  4. package/dist/body-sync-gate.js +53 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +193 -0
  7. package/dist/config-env.d.ts +5 -0
  8. package/dist/config-env.js +37 -0
  9. package/dist/connection-pool.d.ts +22 -0
  10. package/dist/connection-pool.js +97 -0
  11. package/dist/connection-scope.d.ts +8 -0
  12. package/dist/connection-scope.js +10 -0
  13. package/dist/context.d.ts +46 -0
  14. package/dist/context.js +15 -0
  15. package/dist/e2e-processor-shim.d.ts +2 -0
  16. package/dist/e2e-processor-shim.js +181 -0
  17. package/dist/emit.d.ts +15 -0
  18. package/dist/emit.js +76 -0
  19. package/dist/events.d.ts +203 -0
  20. package/dist/events.js +1 -0
  21. package/dist/failure-kind.d.ts +10 -0
  22. package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
  23. package/dist/handlers/append-sent-message.d.ts +3 -0
  24. package/dist/handlers/append-sent-message.js +152 -0
  25. package/dist/handlers/delete-account-objects.d.ts +3 -0
  26. package/dist/handlers/delete-account-objects.js +35 -0
  27. package/dist/handlers/empty-trash.d.ts +14 -0
  28. package/dist/handlers/empty-trash.js +217 -0
  29. package/dist/handlers/flag-push.d.ts +38 -0
  30. package/dist/handlers/flag-push.js +195 -0
  31. package/dist/handlers/imported-folder.d.ts +4 -0
  32. package/dist/handlers/imported-folder.js +23 -0
  33. package/dist/handlers/mailbox-management.d.ts +6 -0
  34. package/dist/handlers/mailbox-management.js +294 -0
  35. package/dist/handlers/mailbox-sync-order.d.ts +21 -0
  36. package/dist/handlers/mailbox-sync-order.js +50 -0
  37. package/dist/handlers/message-copy.d.ts +40 -0
  38. package/dist/handlers/message-copy.js +371 -0
  39. package/dist/handlers/message-delete-terminal.d.ts +60 -0
  40. package/dist/handlers/message-delete-terminal.js +77 -0
  41. package/dist/handlers/message-delete.d.ts +57 -0
  42. package/dist/handlers/message-delete.js +520 -0
  43. package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
  44. package/dist/handlers/message-move-terminal.js +94 -0
  45. package/dist/handlers/message-move.d.ts +146 -0
  46. package/dist/handlers/message-move.js +490 -0
  47. package/dist/handlers/placement-move-push.d.ts +62 -0
  48. package/dist/handlers/placement-move-push.js +307 -0
  49. package/dist/handlers/probe-message-id.d.ts +2 -0
  50. package/dist/handlers/probe-message-id.js +6 -0
  51. package/dist/handlers/sync-mailboxes.d.ts +45 -0
  52. package/dist/handlers/sync-mailboxes.js +233 -0
  53. package/dist/handlers/sync-message-body.d.ts +23 -0
  54. package/dist/handlers/sync-message-body.js +182 -0
  55. package/dist/handlers/sync-messages.d.ts +29 -0
  56. package/dist/handlers/sync-messages.js +285 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +67 -0
  59. package/dist/lambda-context.d.ts +8 -0
  60. package/dist/lambda-context.js +33 -0
  61. package/dist/mailbox-row.d.ts +9 -0
  62. package/dist/mailbox-row.js +6 -0
  63. package/dist/poller.d.ts +1 -0
  64. package/dist/poller.js +51 -0
  65. package/dist/process.d.ts +6 -0
  66. package/dist/process.js +3 -0
  67. package/dist/processor.d.ts +3 -0
  68. package/dist/processor.js +44 -0
  69. package/dist/queues.d.ts +10 -0
  70. package/dist/queues.js +1 -0
  71. package/dist/s3-account-objects.d.ts +3 -0
  72. package/dist/s3-account-objects.js +26 -0
  73. package/dist/scheduler/attachment-sweep.d.ts +11 -0
  74. package/dist/scheduler/attachment-sweep.js +23 -0
  75. package/dist/scheduler/calendar-subscriptions.d.ts +22 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +29 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +9 -28
  79. package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
  80. package/dist/scheduler/decide-due.js +17 -0
  81. package/dist/scheduler/handler.d.ts +21 -0
  82. package/dist/scheduler/handler.js +50 -0
  83. package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
  84. package/dist/scheduler/loop.js +8 -0
  85. package/dist/scheduler/run-tick.d.ts +66 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/dist/scheduler/runner.js +102 -0
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/scheduler/tick.d.ts +5 -0
  92. package/dist/scheduler/tick.js +5 -0
  93. package/dist/sqs-queues.d.ts +4 -0
  94. package/dist/sqs-queues.js +22 -0
  95. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  96. package/dist/with-oauth-lifecycle-deps.js +17 -0
  97. package/dist/with-oauth-lifecycle.d.ts +56 -0
  98. package/dist/with-oauth-lifecycle.js +92 -0
  99. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  100. package/dist/worker-version.js +12 -0
  101. package/package.json +79 -69
  102. package/src/account-check.test.ts +0 -149
  103. package/src/account-check.ts +0 -101
  104. package/src/body-sync-gate.test.ts +0 -185
  105. package/src/body-sync-gate.ts +0 -86
  106. package/src/cli.ts +0 -218
  107. package/src/connection-scope.test.ts +0 -266
  108. package/src/connection-scope.ts +0 -314
  109. package/src/credential-less-sync-fence.test.ts +0 -152
  110. package/src/e2e-processor-shim.ts +0 -232
  111. package/src/emit.test.ts +0 -145
  112. package/src/emit.ts +0 -115
  113. package/src/events.ts +0 -252
  114. package/src/failure-kind.test.ts +0 -42
  115. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  116. package/src/handlers/append-sent-message.ts +0 -264
  117. package/src/handlers/delete-account-objects.test.ts +0 -126
  118. package/src/handlers/delete-account-objects.ts +0 -96
  119. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  120. package/src/handlers/empty-trash.ts +0 -352
  121. package/src/handlers/flag-push.ts +0 -285
  122. package/src/handlers/imported-folder.test.ts +0 -63
  123. package/src/handlers/imported-folder.ts +0 -38
  124. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  125. package/src/handlers/mailbox-management.ts +0 -486
  126. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  127. package/src/handlers/mailbox-sync-order.ts +0 -65
  128. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  129. package/src/handlers/message-copy.ts +0 -569
  130. package/src/handlers/message-delete-terminal.ts +0 -147
  131. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  132. package/src/handlers/message-delete.ts +0 -888
  133. package/src/handlers/message-move.sqlite.test.ts +0 -271
  134. package/src/handlers/message-move.test.ts +0 -216
  135. package/src/handlers/message-move.ts +0 -755
  136. package/src/handlers/placement-move-push.test.ts +0 -137
  137. package/src/handlers/placement-move-push.ts +0 -478
  138. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  139. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  140. package/src/handlers/probe-message-id.ts +0 -15
  141. package/src/handlers/sync-mailboxes.test.ts +0 -183
  142. package/src/handlers/sync-mailboxes.ts +0 -425
  143. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  144. package/src/handlers/sync-message-body.test.ts +0 -150
  145. package/src/handlers/sync-message-body.ts +0 -366
  146. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  147. package/src/handlers/sync-messages.ts +0 -537
  148. package/src/index.test.ts +0 -22
  149. package/src/index.ts +0 -82
  150. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  151. package/src/mailbox-row.ts +0 -20
  152. package/src/pending-sync.sqlite.test.ts +0 -165
  153. package/src/poller.ts +0 -55
  154. package/src/processor.ts +0 -62
  155. package/src/queues.ts +0 -40
  156. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  157. package/src/scheduler/attachment-sweep.ts +0 -46
  158. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  159. package/src/scheduler/calendar-subscriptions.ts +0 -103
  160. package/src/scheduler/config.test.ts +0 -40
  161. package/src/scheduler/decide-due.test.ts +0 -44
  162. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  163. package/src/scheduler/handler.ts +0 -69
  164. package/src/scheduler/loop.test.ts +0 -143
  165. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  166. package/src/scheduler/run-tick.ts +0 -281
  167. package/src/scheduler/runner.ts +0 -125
  168. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  169. package/src/stranded-placement.sqlite.test.ts +0 -799
  170. package/src/test-helpers/test-world.ts +0 -425
  171. package/src/test-helpers/worker-queues.ts +0 -11
  172. package/src/thread-join.sqlite.test.ts +0 -106
  173. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  174. package/src/with-oauth-lifecycle-deps.ts +0 -71
  175. package/src/with-oauth-lifecycle.ts +0 -158
  176. package/tsconfig.json +0 -8
@@ -1,147 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { CalendarFeedFetcher } from "@remit/calendar-service";
4
- import type { CalendarCollectionItem } from "@remit/data-ports";
5
- import { CalendarSource } from "@remit/domain-enums";
6
- import type { Logger } from "@remit/logger";
7
- import { openStore } from "../test-helpers/test-world.js";
8
- import { refreshDueCalendarSubscriptions } from "./calendar-subscriptions.js";
9
-
10
- const INTERVAL_MS = 15 * 60 * 1000;
11
- const NOW = 1_800_000_000_000;
12
-
13
- const FEED = `${[
14
- "BEGIN:VCALENDAR",
15
- "VERSION:2.0",
16
- "PRODID:-//Example//Published Calendar//EN",
17
- "BEGIN:VEVENT",
18
- "UID:ferry@calendar.example",
19
- "DTSTAMP:20260901T000000Z",
20
- "DTSTART:20260908T070000Z",
21
- "DTEND:20260908T080000Z",
22
- "SUMMARY:Ferry crossing",
23
- "END:VEVENT",
24
- "END:VCALENDAR",
25
- ].join("\r\n")}\r\n`;
26
-
27
- const logged = (): { log: Logger; lines: string[] } => {
28
- const lines: string[] = [];
29
- const capture = (...args: unknown[]) => {
30
- lines.push(JSON.stringify(args));
31
- };
32
- const log = {
33
- info: capture,
34
- warn: capture,
35
- error: capture,
36
- debug: capture,
37
- fatal: capture,
38
- trace: capture,
39
- child: () => log,
40
- } as unknown as Logger;
41
- return { log, lines };
42
- };
43
-
44
- type Client = Awaited<ReturnType<typeof openStore>>["client"];
45
-
46
- const subscription = async (
47
- client: Client,
48
- urlSegment: string,
49
- checkedAt: number,
50
- ): Promise<CalendarCollectionItem> => {
51
- const created = await client.calendarCollection.create({
52
- accountConfigId: "account-config-1",
53
- urlSegment,
54
- displayName: urlSegment,
55
- source: CalendarSource.Subscribed,
56
- subscriptionUrl: `https://calendar.example/${urlSegment}/private-feedfacecafe.ics`,
57
- subscriptionEnabled: true,
58
- });
59
- return client.calendarCollection.update(
60
- created.accountConfigId,
61
- created.calendarId,
62
- { subscriptionCheckedAt: checkedAt },
63
- );
64
- };
65
-
66
- const recording =
67
- (asked: string[], answer: (url: string) => Response): CalendarFeedFetcher =>
68
- async (url) => {
69
- asked.push(url);
70
- return answer(url);
71
- };
72
-
73
- describe("refreshDueCalendarSubscriptions", () => {
74
- it("fetches the subscriptions that are due and skips the rest", async (t) => {
75
- const { client, close } = await openStore();
76
- t.after(close);
77
- const due = await subscription(client, "due", NOW - INTERVAL_MS);
78
- await subscription(client, "fresh", NOW - 60_000);
79
- const paused = await subscription(client, "paused", 0);
80
- await client.calendarCollection.update(
81
- paused.accountConfigId,
82
- paused.calendarId,
83
- { subscriptionEnabled: false },
84
- );
85
- const asked: string[] = [];
86
-
87
- const tally = await refreshDueCalendarSubscriptions(
88
- {
89
- calendarCollection: client.calendarCollection,
90
- calendarStore: client.calendarStore,
91
- log: logged().log,
92
- fetcher: recording(asked, () => new Response(FEED, { status: 200 })),
93
- },
94
- NOW,
95
- INTERVAL_MS,
96
- );
97
-
98
- assert.deepEqual(asked, [due.subscriptionUrl]);
99
- assert.deepEqual(tally, { refreshed: 1, failed: 0, notDue: 1 });
100
- const objects = await client.calendarObject.listByCalendar(due.calendarId);
101
- assert.deepEqual(
102
- objects.map((object) => object.summary),
103
- ["Ferry crossing"],
104
- );
105
- });
106
-
107
- it("counts a feed that fails, keeps refreshing the others, and never logs an address", async (t) => {
108
- const { client, close } = await openStore();
109
- t.after(close);
110
- const broken = await subscription(client, "broken", 0);
111
- const working = await subscription(client, "working", 0);
112
- const { log, lines } = logged();
113
-
114
- const tally = await refreshDueCalendarSubscriptions(
115
- {
116
- calendarCollection: client.calendarCollection,
117
- calendarStore: client.calendarStore,
118
- log,
119
- fetcher: recording([], (url) =>
120
- url === broken.subscriptionUrl
121
- ? new Response("gone", { status: 410 })
122
- : new Response(FEED, { status: 200 }),
123
- ),
124
- },
125
- NOW,
126
- INTERVAL_MS,
127
- );
128
-
129
- assert.deepEqual(tally, { refreshed: 1, failed: 1, notDue: 0 });
130
- const stored = await client.calendarCollection.get(
131
- broken.accountConfigId,
132
- broken.calendarId,
133
- );
134
- assert.equal(stored.subscriptionError, "the feed answered HTTP 410");
135
- assert.equal(stored.subscriptionCheckedAt, NOW);
136
- const refreshed = await client.calendarCollection.get(
137
- working.accountConfigId,
138
- working.calendarId,
139
- );
140
- assert.equal(refreshed.subscriptionFetchedAt, NOW);
141
- assert.ok(lines.some((line) => line.includes(broken.calendarId)));
142
- assert.equal(
143
- lines.some((line) => line.includes("private-feedfacecafe")),
144
- false,
145
- );
146
- });
147
- });
@@ -1,103 +0,0 @@
1
- import type { RemitClient } from "@remit/backend/client";
2
- import {
3
- type CalendarFeedFetcher,
4
- isSubscriptionDue,
5
- refreshCalendarSubscription,
6
- } from "@remit/calendar-service";
7
- import type {
8
- ICalendarCollectionRepository,
9
- ICalendarStore,
10
- } from "@remit/data-ports";
11
- import type { Logger } from "@remit/logger";
12
- import pMap from "p-map";
13
-
14
- const REFRESH_CONCURRENCY = 4;
15
-
16
- export interface CalendarSubscriptionTally {
17
- refreshed: number;
18
- failed: number;
19
- notDue: number;
20
- }
21
-
22
- export interface CalendarSubscriptionRefreshDeps {
23
- calendarCollection: Pick<
24
- ICalendarCollectionRepository,
25
- "listEnabledSubscriptions"
26
- >;
27
- calendarStore: ICalendarStore;
28
- log: Logger;
29
- fetcher?: CalendarFeedFetcher;
30
- }
31
-
32
- /**
33
- * Re-reads every enabled subscription whose last fetch is at least
34
- * `intervalMs` old. A disabled subscription is never listed, which is what
35
- * stops its refresh while leaving its events in place.
36
- *
37
- * One feed's failure is recorded on its own collection and counted here; it
38
- * never stops the others. The log names the calendar and the reason, never the
39
- * URL, which for a Google secret address is the whole credential.
40
- */
41
- export const refreshDueCalendarSubscriptions = async (
42
- deps: CalendarSubscriptionRefreshDeps,
43
- now: number,
44
- intervalMs: number,
45
- ): Promise<CalendarSubscriptionTally> => {
46
- const enabled = await deps.calendarCollection.listEnabledSubscriptions();
47
- const due = enabled.filter((collection) =>
48
- isSubscriptionDue(collection, now, intervalMs),
49
- );
50
-
51
- const outcomes = await pMap(
52
- due,
53
- (collection) =>
54
- refreshCalendarSubscription(
55
- deps.calendarStore,
56
- collection,
57
- now,
58
- deps.fetcher,
59
- ).then(
60
- (refresh) => {
61
- if (refresh.ok) return true;
62
- deps.log.warn(
63
- { calendarId: collection.calendarId, reason: refresh.reason },
64
- "Calendar subscription could not be refreshed",
65
- );
66
- return false;
67
- },
68
- (error: unknown) => {
69
- deps.log.error(
70
- { calendarId: collection.calendarId, error },
71
- "Calendar subscription refresh failed",
72
- );
73
- return false;
74
- },
75
- ),
76
- { concurrency: REFRESH_CONCURRENCY },
77
- );
78
-
79
- return {
80
- refreshed: outcomes.filter(Boolean).length,
81
- failed: outcomes.filter((ok) => !ok).length,
82
- notDue: enabled.length - due.length,
83
- };
84
- };
85
-
86
- export const buildCalendarSubscriptionRefresh =
87
- (
88
- client: Pick<RemitClient, "calendarCollection" | "calendarStore">,
89
- log: Logger,
90
- ): ((
91
- now: number,
92
- intervalMs: number,
93
- ) => Promise<CalendarSubscriptionTally>) =>
94
- (now, intervalMs) =>
95
- refreshDueCalendarSubscriptions(
96
- {
97
- calendarCollection: client.calendarCollection,
98
- calendarStore: client.calendarStore,
99
- log,
100
- },
101
- now,
102
- intervalMs,
103
- );
@@ -1,40 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
4
-
5
- describe("getTickIntervalMs", () => {
6
- it("defaults to 1 hour when unset", () => {
7
- assert.equal(getTickIntervalMs({}), 60 * 60 * 1000);
8
- });
9
-
10
- it("reads MAILBOX_SYNC_TICK_INTERVAL_SECONDS", () => {
11
- assert.equal(
12
- getTickIntervalMs({ MAILBOX_SYNC_TICK_INTERVAL_SECONDS: "60" }),
13
- 60_000,
14
- );
15
- });
16
-
17
- it("falls back to the default on a non-positive or non-numeric value", () => {
18
- assert.equal(
19
- getTickIntervalMs({ MAILBOX_SYNC_TICK_INTERVAL_SECONDS: "0" }),
20
- 60 * 60 * 1000,
21
- );
22
- assert.equal(
23
- getTickIntervalMs({ MAILBOX_SYNC_TICK_INTERVAL_SECONDS: "nope" }),
24
- 60 * 60 * 1000,
25
- );
26
- });
27
- });
28
-
29
- describe("getOfflineIntervalMs", () => {
30
- it("defaults to 12 hours when unset", () => {
31
- assert.equal(getOfflineIntervalMs({}), 12 * 60 * 60 * 1000);
32
- });
33
-
34
- it("reads MAILBOX_SYNC_OFFLINE_INTERVAL_SECONDS", () => {
35
- assert.equal(
36
- getOfflineIntervalMs({ MAILBOX_SYNC_OFFLINE_INTERVAL_SECONDS: "3600" }),
37
- 3_600_000,
38
- );
39
- });
40
- });
@@ -1,44 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { isSyncDue } from "./decide-due.js";
4
-
5
- const OFFLINE_INTERVAL_MS = 12 * 60 * 60 * 1000;
6
- const NOW = 1_700_000_000_000;
7
-
8
- describe("isSyncDue", () => {
9
- it("is always due when the account has never synced", () => {
10
- assert.equal(isSyncDue({ accountId: "a" }, NOW, OFFLINE_INTERVAL_MS), true);
11
- });
12
-
13
- it("is due once the sync is older than the offline interval", () => {
14
- const account = {
15
- accountId: "a",
16
- lastSyncAt: NOW - OFFLINE_INTERVAL_MS - 1,
17
- };
18
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), true);
19
- });
20
-
21
- it("is not due within the offline interval", () => {
22
- const account = {
23
- accountId: "a",
24
- lastSyncAt: NOW - 60_000,
25
- };
26
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), false);
27
- });
28
-
29
- it("is due exactly at the threshold", () => {
30
- const account = {
31
- accountId: "a",
32
- lastSyncAt: NOW - OFFLINE_INTERVAL_MS,
33
- };
34
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), true);
35
- });
36
-
37
- it("is not due one millisecond before the threshold", () => {
38
- const account = {
39
- accountId: "a",
40
- lastSyncAt: NOW - OFFLINE_INTERVAL_MS + 1,
41
- };
42
- assert.equal(isSyncDue(account, NOW, OFFLINE_INTERVAL_MS), false);
43
- });
44
- });
@@ -1,196 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { randomBytes } from "node:crypto";
3
- import { after, before, beforeEach, describe, it } from "node:test";
4
- import { finishHalfDeletedAccounts } from "@remit/backend/account-deletion";
5
- import type { RemitClient } from "@remit/backend/client";
6
- import { importConfig } from "@remit/config-transfer";
7
- import type { AccountItem } from "@remit/data-ports";
8
- import {
9
- failingQueue,
10
- type RecordingQueue,
11
- recordingQueue,
12
- } from "@remit/data-ports/recording-queue";
13
- import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
14
- import { noopLogger } from "@remit/logger";
15
- import { openStore } from "../test-helpers/test-world.js";
16
-
17
- let store: Awaited<ReturnType<typeof openStore>>;
18
- let client: RemitClient;
19
-
20
- before(async () => {
21
- store = await openStore();
22
- client = store.client;
23
- });
24
-
25
- after(() => store.close());
26
-
27
- const storedId = (): string => randomBytes(13).toString("hex").slice(0, 25);
28
-
29
- const configDocument = (accountId: string, email: string) => ({
30
- kind: "reader.config",
31
- schemaVersion: 1,
32
- generator: {
33
- app: "reader",
34
- version: "v0.1.0",
35
- exportedAt: "2026-08-27T09:15:00+02:00",
36
- },
37
- provenance: { accountConfigId: storedId(), instance: "reader.example.com" },
38
- accountConfig: { name: "Imported" },
39
- accounts: [
40
- {
41
- accountId,
42
- email,
43
- username: email,
44
- authType: "password",
45
- credentials: { required: "password" },
46
- isActive: true,
47
- imap: { host: "imap.example.com", port: 993, tls: true, startTls: false },
48
- smtp: {
49
- enabled: false,
50
- host: "",
51
- port: 587,
52
- tls: false,
53
- startTls: false,
54
- username: "",
55
- },
56
- displayName: "",
57
- muted: null,
58
- composeLanguages: [],
59
- signature: { plainText: "", html: "" },
60
- folderRoles: [],
61
- folderOverrides: [],
62
- pinnedFolders: [],
63
- },
64
- ],
65
- labels: [],
66
- filters: [],
67
- addressFlags: [],
68
- });
69
-
70
- const seedAccount = (
71
- accountConfigId: string,
72
- name: string,
73
- extra: { deletedAt?: number } = {},
74
- ): Promise<AccountItem> =>
75
- client.account.create({
76
- accountConfigId,
77
- username: `${name}@example.com`,
78
- email: `${name}@example.com`,
79
- authType: AccountAuthType.Password,
80
- passwordHash: '{"ciphertext":"x"}',
81
- imapHost: "imap.example.com",
82
- imapPort: 993,
83
- imapTls: true,
84
- imapStartTls: false,
85
- isActive: true,
86
- connectionState: ConnectionState.Authenticated,
87
- ...extra,
88
- });
89
-
90
- const importAccount = async (email: string): Promise<AccountItem> => {
91
- const accountConfigId = storedId();
92
- const accountId = storedId();
93
- const outcome = await importConfig(
94
- {
95
- repositories: client,
96
- appointFolderRole: async () => {},
97
- },
98
- {
99
- accountConfigId,
100
- userId: accountConfigId,
101
- document: configDocument(accountId, email),
102
- mode: "apply",
103
- onExisting: "abort",
104
- },
105
- );
106
- assert.equal(outcome.outcome, "report");
107
- return client.account.get(accountId);
108
- };
109
-
110
- const heal = (queue: RecordingQueue) =>
111
- finishHalfDeletedAccounts({
112
- account: client.account,
113
- configImport: client.configImport,
114
- queue,
115
- log: noopLogger,
116
- });
117
-
118
- describe("finishing an account deleted before #1384 on scheduler start", () => {
119
- let halfDeleted: AccountItem;
120
- let live: AccountItem;
121
- let deleted: AccountItem;
122
- let imported: AccountItem;
123
-
124
- beforeEach(async () => {
125
- const accountConfigId = storedId();
126
- live = await seedAccount(accountConfigId, `live-${storedId()}`);
127
- halfDeleted = await seedAccount(accountConfigId, `gone-${storedId()}`);
128
- await client.account.update(halfDeleted.accountId, { isActive: false });
129
- deleted = await seedAccount(accountConfigId, `deleted-${storedId()}`, {
130
- deletedAt: 1_700_000_000_000,
131
- });
132
- await client.account.update(deleted.accountId, { isActive: false });
133
- imported = await importAccount(`imported-${storedId()}@example.com`);
134
- });
135
-
136
- const allAccounts = () =>
137
- client.account.get([
138
- halfDeleted.accountId,
139
- live.accountId,
140
- deleted.accountId,
141
- imported.accountId,
142
- ]);
143
-
144
- it("tombstones it and asks for the same purge a delete asks for", async () => {
145
- const queue = recordingQueue();
146
-
147
- await heal(queue);
148
-
149
- const purges = queue.sent.map((message) => message.body);
150
- assert.ok(
151
- purges.some(
152
- (body) =>
153
- body.type === "AccountDataPurge" &&
154
- body.accountId === halfDeleted.accountId &&
155
- body.accountConfigId === halfDeleted.accountConfigId,
156
- ),
157
- );
158
- const byId = new Map(
159
- (await allAccounts()).map((account) => [account.accountId, account]),
160
- );
161
- assert.ok(byId.get(halfDeleted.accountId)?.deletedAt);
162
- assert.equal(byId.get(live.accountId)?.deletedAt, undefined);
163
- assert.equal(byId.get(imported.accountId)?.deletedAt, undefined);
164
- assert.equal(byId.get(deleted.accountId)?.deletedAt, 1_700_000_000_000);
165
- assert.ok(
166
- !purges.some((body) =>
167
- [live.accountId, imported.accountId, deleted.accountId].includes(
168
- String(body.accountId),
169
- ),
170
- ),
171
- );
172
- });
173
-
174
- it("does nothing for it on the next start", async () => {
175
- await heal(recordingQueue());
176
- const queue = recordingQueue();
177
-
178
- await heal(queue);
179
-
180
- assert.ok(
181
- !queue.sent.some(
182
- (message) => message.body.accountId === halfDeleted.accountId,
183
- ),
184
- );
185
- });
186
-
187
- it("fails the start and leaves it to be found again when the purge cannot be asked for", async () => {
188
- await assert.rejects(
189
- heal(failingQueue(new Error("queue unreachable"))),
190
- /queue unreachable/,
191
- );
192
-
193
- const after = await client.account.get(halfDeleted.accountId);
194
- assert.equal(after.deletedAt, undefined);
195
- });
196
- });
@@ -1,69 +0,0 @@
1
- import { getClient } from "@remit/backend/client";
2
- import { createLogger } from "@remit/logger";
3
- import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
4
- import type { ScheduledHandler } from "aws-lambda";
5
- import { createEmitEvent } from "../emit.js";
6
- import {
7
- sqsWorkerQueues,
8
- type WorkerQueues,
9
- workerQueueUrlsFromEnv,
10
- } from "../queues.js";
11
- import { buildAttachmentSweep } from "./attachment-sweep.js";
12
- import { buildCalendarSubscriptionRefresh } from "./calendar-subscriptions.js";
13
- import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
14
- import { runSchedulerTick } from "./run-tick.js";
15
- import { redriveStrandedPlacements } from "./stranded-placement-redrive.js";
16
-
17
- installNodeLogger();
18
-
19
- const log = createLogger();
20
-
21
- /**
22
- * EventBridge-scheduled entry point for the periodic mailbox-sync tick
23
- * (#1247, restructured #1251). Ticks at `MAILBOX_SYNC_TICK_INTERVAL_SECONDS`
24
- * (rate schedule, wired in infra/stacks/dev/stacks/remit-worker-stack.ts) and
25
- * delegates the actual decision + enqueue to `runSchedulerTick` — the same
26
- * function the timer loop calls on a deployment with no EventBridge (see
27
- * `runner.ts`), so every deployment runs one code path.
28
- *
29
- * Uses the EventBridge event's own `time` (the scheduled fire time) as the
30
- * tick's `now`, rather than `Date.now()` at processing time. This is what
31
- * keeps `buildScheduledSyncDedupId`'s time bucket aligned to the schedule
32
- * instead of wall-clock/Lambda-cold-start jitter (review #1250): every
33
- * distinct scheduled firing gets a `time` exactly `tickIntervalMs` apart
34
- * from the last, so consecutive ticks always land in different buckets,
35
- * while a genuine retry of the same invocation carries the same `time` and
36
- * correctly dedupes.
37
- */
38
- export const buildScheduledHandler = (queues: WorkerQueues): ScheduledHandler =>
39
- withTelemetry(async (event) => {
40
- const client = await getClient();
41
- const now = Date.parse(event.time);
42
-
43
- await runSchedulerTick({
44
- accountService: client.account,
45
- queue: queues.mailboxes,
46
- log,
47
- tickIntervalMs: getTickIntervalMs(),
48
- offlineIntervalMs: getOfflineIntervalMs(),
49
- now,
50
- // The hosted side is the one that needs this: a browser PUTs straight
51
- // to block storage, so nothing refuses an upload against a draft that is
52
- // already gone and this sweep is the only thing that collects it.
53
- sweepAttachments: buildAttachmentSweep(client, log),
54
- refreshCalendarSubscriptions: buildCalendarSubscriptionRefresh(
55
- client,
56
- log,
57
- ),
58
- redriveStrandedPlacements: (account, tickNow) =>
59
- redriveStrandedPlacements(
60
- { client, emit: createEmitEvent(queues), log },
61
- account,
62
- tickNow,
63
- ),
64
- });
65
- });
66
-
67
- export const handler: ScheduledHandler = buildScheduledHandler(
68
- sqsWorkerQueues(workerQueueUrlsFromEnv()),
69
- );