@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.
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 -521
  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,39 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import type { ConfigExportIdentity } from "@remit/config-transfer";
4
-
5
- const DEFAULT_CONTROL_DIR = "/data/control";
6
-
7
- /**
8
- * The version a file says it was written by. The updater owns this fact: it
9
- * reads the tag from `.env` and writes it into `state.json`, which is the only
10
- * honest source across an update that rewrites that tag underneath a
11
- * long-lived process. With no state file there is nothing authoritative to
12
- * stamp, and an export says so rather than inventing a version an importing
13
- * reader would then trust.
14
- */
15
- const runningVersion = (): string => {
16
- const dir = process.env.REMIT_UPDATE_CONTROL_DIR ?? DEFAULT_CONTROL_DIR;
17
- let raw: string;
18
- try {
19
- raw = readFileSync(join(dir, "state.json"), "utf8");
20
- } catch {
21
- return "unknown";
22
- }
23
- const state: unknown = JSON.parse(raw);
24
- if (typeof state !== "object" || state === null) return "unknown";
25
- const version = (state as { currentVersion?: unknown }).currentVersion;
26
- return typeof version === "string" && version.length > 0
27
- ? version
28
- : "unknown";
29
- };
30
-
31
- /** Who wrote a configuration file, and from where. Provenance only. */
32
- export const exportIdentity = (
33
- now: Date = new Date(),
34
- ): ConfigExportIdentity => ({
35
- app: "reader",
36
- version: runningVersion(),
37
- exportedAt: now.toISOString(),
38
- instance: process.env.PUBLIC_ORIGIN ?? "",
39
- });
@@ -1,40 +0,0 @@
1
- import { type AnchorPayload, buildMessageAnchor } from "@remit/search-service";
2
- import {
3
- buildEmbeddingServiceFromEnv,
4
- buildVectorStoreFromEnv,
5
- } from "@remit/search-service/from-env";
6
-
7
- /**
8
- * Build a filter's persisted semantic anchor from an already-indexed message's
9
- * chunk vectors (RFC 034 Decision 2.1), or `null` when the message has no
10
- * indexed chunks. Never embeds anything new — the vectors already exist from
11
- * index time.
12
- *
13
- * This is the only piece of filter CRUD not on the `RemitClient` port bundle:
14
- * the vector store and embedder are selected by env, independent of
15
- * `DATA_BACKEND`, so it works unchanged on either backend. The filter and
16
- * anchor rows themselves are written through
17
- * `client.filter`/`client.filterAnchor` so they land in the same backend as
18
- * everything else.
19
- */
20
- export type BuildFilterAnchor = (
21
- accountConfigId: string,
22
- anchorMessageId: string,
23
- ) => Promise<AnchorPayload | null>;
24
-
25
- let cached: BuildFilterAnchor | null = null;
26
-
27
- const build = (): BuildFilterAnchor => {
28
- const embedder = buildEmbeddingServiceFromEnv();
29
- const store = buildVectorStoreFromEnv(embedder.dimensions);
30
- return (accountConfigId, anchorMessageId) =>
31
- buildMessageAnchor({ store }, { accountConfigId, anchorMessageId });
32
- };
33
-
34
- export const buildFilterAnchor: BuildFilterAnchor = (
35
- accountConfigId,
36
- anchorMessageId,
37
- ) => {
38
- if (!cached) cached = build();
39
- return cached(accountConfigId, anchorMessageId);
40
- };
@@ -1,126 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { type FireAndForgetLogger, fireAndForget } from "./fire-and-forget.js";
4
-
5
- interface LogCall {
6
- fields: Record<string, unknown>;
7
- message: string;
8
- }
9
-
10
- const createLoggerSpy = () => {
11
- const error: LogCall[] = [];
12
- const logger: FireAndForgetLogger = {
13
- error: (fields: Record<string, unknown>, message: string): void => {
14
- error.push({ fields, message });
15
- },
16
- };
17
- return { error, logger };
18
- };
19
-
20
- describe("fireAndForget", () => {
21
- it("runs the work and stays quiet on success", async () => {
22
- const { error, logger } = createLoggerSpy();
23
- let ran = false;
24
-
25
- await fireAndForget(
26
- async () => {
27
- ran = true;
28
- },
29
- { source: "test", message: "should not log", logger },
30
- );
31
-
32
- assert.equal(ran, true);
33
- assert.equal(error.length, 0);
34
- });
35
-
36
- it("does NOT reject when the work throws", async () => {
37
- const { logger } = createLoggerSpy();
38
-
39
- await assert.doesNotReject(
40
- fireAndForget(
41
- async () => {
42
- throw new Error("boom");
43
- },
44
- { source: "test", message: "contained", logger },
45
- ),
46
- );
47
- });
48
-
49
- it("logs the failure loudly with the alertable structured fields and ids", async () => {
50
- const { error, logger } = createLoggerSpy();
51
- const econnrefused = Object.assign(new Error(""), {
52
- name: "AggregateError",
53
- code: "ECONNREFUSED",
54
- });
55
-
56
- await fireAndForget(
57
- async () => {
58
- throw econnrefused;
59
- },
60
- {
61
- source: "config_load",
62
- message: "Failed to trigger account sync on config load",
63
- ids: { accountId: "acc-a", accountConfigId: "config-1" },
64
- logger,
65
- },
66
- );
67
-
68
- assert.equal(error.length, 1);
69
- const call = error[0];
70
- if (!call) throw new Error("expected an error log");
71
- assert.equal(call.fields.alert, "sync_trigger_failed");
72
- assert.equal(call.fields.source, "config_load");
73
- assert.equal(call.fields.accountId, "acc-a");
74
- assert.equal(call.fields.accountConfigId, "config-1");
75
- assert.equal(call.fields.errorName, "AggregateError");
76
- assert.equal(call.fields.errorCode, "ECONNREFUSED");
77
- assert.equal(call.message, "Failed to trigger account sync on config load");
78
- });
79
-
80
- it("reads the SDK error code from the .Code field when present", async () => {
81
- const { error, logger } = createLoggerSpy();
82
- const sdkError = Object.assign(new Error("boom"), {
83
- name: "QueueDoesNotExist",
84
- Code: "AWS.SimpleQueueService.NonExistentQueue",
85
- });
86
-
87
- await fireAndForget(
88
- async () => {
89
- throw sdkError;
90
- },
91
- { source: "test", message: "contained", logger },
92
- );
93
-
94
- const call = error[0];
95
- if (!call) throw new Error("expected an error log");
96
- assert.equal(
97
- call.fields.errorCode,
98
- "AWS.SimpleQueueService.NonExistentQueue",
99
- );
100
- });
101
-
102
- it("never leaks an unhandled rejection even when void-fired", async () => {
103
- const leaked: unknown[] = [];
104
- const onUnhandled = (reason: unknown): void => {
105
- leaked.push(reason);
106
- };
107
- process.on("unhandledRejection", onUnhandled);
108
-
109
- try {
110
- // Fire-and-forget exactly as a request handler does: no await.
111
- void fireAndForget(
112
- async () => {
113
- throw Object.assign(new Error(""), { code: "ECONNREFUSED" });
114
- },
115
- { source: "test", message: "contained", logger: { error: () => {} } },
116
- );
117
- // Let the microtask queue drain so any escaped rejection would surface.
118
- await new Promise((resolve) => setImmediate(resolve));
119
- await new Promise((resolve) => setImmediate(resolve));
120
- } finally {
121
- process.off("unhandledRejection", onUnhandled);
122
- }
123
-
124
- assert.equal(leaked.length, 0);
125
- });
126
- });
@@ -1,79 +0,0 @@
1
- import { inspect } from "node:util";
2
- import { logger } from "@remit/logger-lambda";
3
-
4
- type StructuredLog = (fields: Record<string, unknown>, message: string) => void;
5
-
6
- export interface FireAndForgetLogger {
7
- error: StructuredLog;
8
- }
9
-
10
- export interface FireAndForgetContext {
11
- /**
12
- * Stable discriminator for the originating call site (e.g. `"config_load"`,
13
- * `"account_create"`). Surfaces on the log line as `source` so a CloudWatch
14
- * metric filter / alarm can attribute a leak to its origin.
15
- */
16
- source: string;
17
- /**
18
- * Message logged when the background work rejects. Should describe the work
19
- * and reassure that the failure is contained (e.g. "...best-effort").
20
- */
21
- message: string;
22
- /**
23
- * Identifiers carried onto the structured error line (accountId,
24
- * accountConfigId, …) so a failure is traceable without a stack dive.
25
- */
26
- ids?: Record<string, unknown>;
27
- /**
28
- * Alertable discriminator on the structured error line, the field CloudWatch
29
- * metric filters key on. Defaults to `"sync_trigger_failed"` for the original
30
- * sync-kick sites; a different fire-and-forget surface (e.g. a flag enqueue)
31
- * must pass its own so it is not mis-attributed to the sync-trigger alarm.
32
- */
33
- alert?: string;
34
- logger?: FireAndForgetLogger;
35
- }
36
-
37
- /**
38
- * Run unawaited background work that MUST NOT be able to fail the request that
39
- * spawned it.
40
- *
41
- * The dev/Lambda runtime shares one event loop across concurrent requests, so a
42
- * rejection escaping an unawaited background promise (a `void trigger()`) lands
43
- * on whatever request happens to be in flight when the deferred work finally
44
- * fails — turning an unrelated READ into a spurious 500. That is exactly how an
45
- * unreachable SQS queue 500'd `/mailboxes`, `/threads`, `/outbox` and `/config`.
46
- *
47
- * This helper is the single containment point: it awaits the work inside a
48
- * try/catch, logs every rejection LOUDLY with the alertable structured fields
49
- * (`alert` — defaulting to `"sync_trigger_failed"`, `source`, the SDK error
50
- * name/code, the caller ids), and ALWAYS resolves to void. Callers `void
51
- * fireAndForget(...)` with no
52
- * chance of an unhandled rejection escaping. Routing all fire-and-forget sites
53
- * through here keeps the containment from regressing one handler at a time.
54
- *
55
- * Use this ONLY for genuine side effects whose failure must not change the
56
- * response. A failure the caller is responsible for (the real DynamoDB read, an
57
- * enqueue that IS the request's purpose) must still propagate and 500.
58
- */
59
- export const fireAndForget = async (
60
- work: () => Promise<unknown>,
61
- context: FireAndForgetContext,
62
- ): Promise<void> => {
63
- await work().catch((error: unknown) => {
64
- const log = context.logger ?? logger;
65
- log.error(
66
- {
67
- alert: context.alert ?? "sync_trigger_failed",
68
- source: context.source,
69
- ...context.ids,
70
- errorName: (error as { name?: string })?.name,
71
- errorCode:
72
- (error as { Code?: string })?.Code ??
73
- (error as { code?: string })?.code,
74
- error: inspect(error),
75
- },
76
- context.message,
77
- );
78
- });
79
- };
@@ -1,36 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { describe, it } from "node:test";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
7
-
8
- const parseEnvFile = (relativePath: string): Record<string, string> => {
9
- const contents = readFileSync(`${repoRoot}${relativePath}`, "utf8");
10
- const entries: Record<string, string> = {};
11
- for (const line of contents.split("\n")) {
12
- const trimmed = line.trim();
13
- if (!trimmed || trimmed.startsWith("#")) continue;
14
- const eq = trimmed.indexOf("=");
15
- if (eq === -1) continue;
16
- entries[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
17
- }
18
- return entries;
19
- };
20
-
21
- describe("local dev SQS routing (localhost-dev-aws.env)", {
22
- // localhost-dev-aws.env is AWS-local-dev config, stripped from the open-core
23
- // tree; skip where it is absent.
24
- skip: !existsSync(`${repoRoot}localhost-dev-aws.env`),
25
- }, () => {
26
- const env = parseEnvFile("localhost-dev-aws.env");
27
-
28
- it("publishes backend writes to the FIFO mailboxes queue the sync consumer drains", () => {
29
- // Prod wires the API's SQS_QUEUE_URL to the mailboxes FIFO queue
30
- // (infra remit-api-stack.ts). Onboarding publishes SYNC_MAILBOXES there;
31
- // the imap worker consumes it. Local dev must mirror that or onboarding
32
- // events land in a queue nothing drains and accounts never sync.
33
- assert.equal(env.SQS_QUEUE_URL, env.SQS_QUEUE_URL_MAILBOXES);
34
- assert.ok(env.SQS_QUEUE_URL?.endsWith(".fifo"));
35
- });
36
- });
@@ -1,98 +0,0 @@
1
- import { SendMessageCommand } from "@aws-sdk/client-sqs";
2
- import type {
3
- CreateOrganizeJobRequestInput,
4
- OrganizeJobRequestItem,
5
- } from "@remit/data-ports";
6
- import { FilterMatchOperator } from "@remit/domain-enums";
7
- import { logger } from "@remit/logger-lambda";
8
- import {
9
- DEFAULT_SEMANTIC_MATCH_THRESHOLD,
10
- NO_ACTION,
11
- } from "@remit/mailbox-service";
12
- import { env } from "expect-env";
13
- import type { RemitClient } from "./data-client.js";
14
- import type { OrganizePredicate } from "./organize.js";
15
- import { sqsClient } from "./sqs.js";
16
-
17
- /**
18
- * How long a finished (or abandoned) job row lingers before the table-wide TTL
19
- * reclaims it (RFC 034 Decision 1). A back-apply runs once and needs no standing
20
- * lifetime; a week is ample for a client to poll the result.
21
- */
22
- const ORGANIZE_JOB_TTL_SECONDS = 7 * 24 * 60 * 60;
23
-
24
- interface JobOwner {
25
- accountConfigId: string;
26
- userId: string;
27
- }
28
-
29
- /**
30
- * An "all like these" pass carries its predicate; a filter job names only the
31
- * filter, which the worker reads when it runs.
32
- */
33
- export type OrganizeJobRequest = JobOwner &
34
- ({ predicate: OrganizePredicate } | { filterId: string });
35
-
36
- /** A filter job's row: the filter id, every predicate column at its sentinel. */
37
- const filterJobColumns = (
38
- filterId: string,
39
- ): Omit<CreateOrganizeJobRequestInput, keyof JobOwner | "ttl"> => ({
40
- filterId,
41
- anchorMessageId: NO_ACTION,
42
- matchOperator: FilterMatchOperator.And,
43
- literalClauses: [],
44
- similarityThreshold: DEFAULT_SEMANTIC_MATCH_THRESHOLD,
45
- actionLabelId: NO_ACTION,
46
- actionMailboxId: NO_ACTION,
47
- });
48
-
49
- const predicateColumns = (
50
- predicate: OrganizePredicate,
51
- ): Omit<CreateOrganizeJobRequestInput, keyof JobOwner | "ttl"> => ({
52
- filterId: NO_ACTION,
53
- ...predicate,
54
- });
55
-
56
- /**
57
- * Write a Pending job row and hand it to the account-fanout worker. Every
58
- * back-apply goes through here: "all like these", a new filter's first pass,
59
- * and Run now on a saved filter.
60
- */
61
- export const queueOrganizeJob = async (
62
- client: Pick<RemitClient, "organizeJobRequest">,
63
- request: OrganizeJobRequest,
64
- ): Promise<OrganizeJobRequestItem> => {
65
- const { accountConfigId, userId } = request;
66
- const columns =
67
- "filterId" in request
68
- ? filterJobColumns(request.filterId)
69
- : predicateColumns(request.predicate);
70
- const job = await client.organizeJobRequest.create({
71
- accountConfigId,
72
- userId,
73
- ...columns,
74
- ttl: Math.floor(Date.now() / 1000) + ORGANIZE_JOB_TTL_SECONDS,
75
- });
76
-
77
- await sqsClient.send(
78
- new SendMessageCommand({
79
- QueueUrl: env.SQS_QUEUE_URL_ACCOUNT_FANOUT,
80
- MessageBody: JSON.stringify({
81
- type: "OrganizeJob",
82
- accountConfigId,
83
- organizeJobId: job.organizeJobId,
84
- }),
85
- }),
86
- );
87
-
88
- // biome-ignore lint/plugin/no-logger-info: a back-apply is an audit-grade signal
89
- logger.info(
90
- {
91
- accountConfigId,
92
- organizeJobId: job.organizeJobId,
93
- filterId: job.filterId,
94
- },
95
- "Organize back-apply job initiated",
96
- );
97
- return job;
98
- };