@remit/smtp-worker 0.0.29 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.test.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * that always sees 1 never reaches the settle at all, and the row dead-letters
5
5
  * at `sending` exactly as it did before #951.
6
6
  *
7
- * The send handler is replaced here, so this file is about the wiring and not
7
+ * The processor is replaced here, so this file is about the wiring and not
8
8
  * about what the handler does with it — and replacing it is also what keeps
9
9
  * the real one's data ports and queue producers out of a unit test.
10
10
  */
@@ -14,11 +14,12 @@ import type { Context, SQSEvent } from "aws-lambda";
14
14
 
15
15
  const receiveCounts: number[] = [];
16
16
 
17
- mock.module("./handlers/send-message.js", {
17
+ mock.module("./processor.js", {
18
18
  namedExports: {
19
- handleSendMessage: async (
19
+ processEvent: async (
20
20
  _event: unknown,
21
21
  _log: unknown,
22
+ _ctx: unknown,
22
23
  receiveCount: number,
23
24
  ): Promise<void> => {
24
25
  receiveCounts.push(receiveCount);
@@ -26,6 +27,10 @@ mock.module("./handlers/send-message.js", {
26
27
  },
27
28
  });
28
29
 
30
+ mock.module("./lambda-context.js", {
31
+ namedExports: { buildLambdaSendContext: async () => ({}) },
32
+ });
33
+
29
34
  const { handler, parseReceiveCount } = await import("./index.js");
30
35
 
31
36
  const context = {
@@ -74,7 +79,7 @@ describe("parseReceiveCount — SQS ApproximateReceiveCount parsing", () => {
74
79
  });
75
80
 
76
81
  describe("the record's delivery count reaches the send handler", () => {
77
- it("threads ApproximateReceiveCount through the processor into handleSendMessage", async () => {
82
+ it("threads ApproximateReceiveCount into the processor", async () => {
78
83
  receiveCounts.length = 0;
79
84
 
80
85
  const response = await handler(sendMessageEvent("3"), context);
package/src/index.ts CHANGED
@@ -9,12 +9,20 @@ import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
9
9
  import type { SQSBatchResponse, SQSEvent } from "aws-lambda";
10
10
  import type { SmtpEvent } from "./events.js";
11
11
  import { smtpFailureKind } from "./failure-kind.js";
12
+ import type { SendMessageContext } from "./handlers/send-message.js";
13
+ import { buildLambdaSendContext } from "./lambda-context.js";
12
14
  import { processEvent } from "./processor.js";
13
15
 
14
16
  installNodeLogger();
15
17
 
16
18
  const log = createLogger();
17
19
 
20
+ let sendContext: Promise<SendMessageContext> | null = null;
21
+ const lambdaSendContext = (): Promise<SendMessageContext> => {
22
+ sendContext ??= buildLambdaSendContext();
23
+ return sendContext;
24
+ };
25
+
18
26
  /**
19
27
  * Parse SQS's `ApproximateReceiveCount` record attribute (1 on first
20
28
  * delivery). Missing/malformed defaults to 1 so a record with no attribute
@@ -44,7 +52,8 @@ export const handler = withTelemetry(
44
52
 
45
53
  const queue = queueNameFromEventSource(record.eventSourceARN);
46
54
  const sendStart = Date.now();
47
- const failed = await processEvent(smtpEvent, log, receiveCount)
55
+ const failed = await lambdaSendContext()
56
+ .then((ctx) => processEvent(smtpEvent, log, ctx, receiveCount))
48
57
  .then(() => {
49
58
  recordQueueEvent({
50
59
  queue,
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { getSendMessageMaxAttempts } from "./lambda-context.js";
4
+
5
+ describe("the retry budget is the queue's, read from the environment", () => {
6
+ it("takes the deployment's own maxReceiveCount when it is set", () => {
7
+ // The e2e stack sets 1 (`deploy/vps/e2e.env`): its pollers hold a record
8
+ // for 300s, so a spec cannot wait out three deliveries.
9
+ assert.equal(
10
+ getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "1" }),
11
+ 1,
12
+ );
13
+ assert.equal(
14
+ getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "5" }),
15
+ 5,
16
+ );
17
+ });
18
+
19
+ it("falls back to remit-smtp's maxReceiveCount when it is unset or unusable", () => {
20
+ assert.equal(getSendMessageMaxAttempts({}), 3);
21
+ assert.equal(
22
+ getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "" }),
23
+ 3,
24
+ );
25
+ assert.equal(
26
+ getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "not-a-number" }),
27
+ 3,
28
+ );
29
+ assert.equal(
30
+ getSendMessageMaxAttempts({ SEND_MESSAGE_MAX_ATTEMPTS: "0" }),
31
+ 3,
32
+ );
33
+ });
34
+ });
@@ -0,0 +1,41 @@
1
+ import { recordSmtpFailure } from "@remit/logger-lambda/metrics";
2
+ import {
3
+ createMailOAuthService,
4
+ microsoftProviderConfig,
5
+ } from "@remit/mail-oauth-service";
6
+ import { createSecretsService } from "@remit/secrets-service";
7
+ import { createKmsDataKeyProvider } from "@remit/secrets-service/kms";
8
+ import { directSmtpSocket } from "@remit/smtp-service";
9
+ import { attemptBudget } from "@remit/sqs-client/attempt-budget";
10
+ import { createStandardQueueProducer } from "@remit/sqs-client/producer";
11
+ import { createStorageService } from "@remit/storage-service/s3";
12
+ import { env } from "expect-env";
13
+ import { buildDataPortsFromEnv } from "./data-ports.js";
14
+ import { createAttachmentReader } from "./handlers/attachment-storage.js";
15
+ import type { SendMessageContext } from "./handlers/send-message.js";
16
+
17
+ export const getSendMessageMaxAttempts = (
18
+ processEnv: NodeJS.ProcessEnv = process.env,
19
+ ): number => attemptBudget("SEND_MESSAGE_MAX_ATTEMPTS", 3, processEnv);
20
+
21
+ export const buildLambdaSendContext =
22
+ async (): Promise<SendMessageContext> => ({
23
+ ports: await buildDataPortsFromEnv(),
24
+ secrets: createSecretsService(createKmsDataKeyProvider(env.KMS_KEY_ID)),
25
+ storage: createAttachmentReader(process.env, createStorageService),
26
+ messageManagement: createStandardQueueProducer({
27
+ queueUrl: env.SQS_QUEUE_URL_MESSAGE_MGMT,
28
+ }),
29
+ tokenService: createMailOAuthService(
30
+ microsoftProviderConfig({
31
+ clientId: process.env.MSOAUTH_CLIENT_ID ?? "",
32
+ clientSecret: process.env.MSOAUTH_CLIENT_SECRET ?? "",
33
+ overrides: process.env.MSOAUTH_TOKEN_ENDPOINT
34
+ ? { tokenEndpoint: process.env.MSOAUTH_TOKEN_ENDPOINT }
35
+ : undefined,
36
+ }),
37
+ ),
38
+ getSocket: directSmtpSocket,
39
+ maxAttempts: getSendMessageMaxAttempts(),
40
+ recordSmtpFailure,
41
+ });
package/src/ports.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type {
2
+ IAccountRepository,
3
+ IAddressRepository,
4
+ IEnvelopeRepository,
5
+ IMessageRepository,
6
+ IOutboxAttachmentRepository,
7
+ IOutboxMessageRepository,
8
+ } from "@remit/data-ports";
9
+
10
+ /**
11
+ * The repositories the SMTP send path reads and writes. One neutral seam so the
12
+ * same handler serves every stack.
13
+ */
14
+ export interface SmtpDataPorts {
15
+ account: IAccountRepository;
16
+ outboxMessage: IOutboxMessageRepository;
17
+ outboxAttachment: IOutboxAttachmentRepository;
18
+ address: IAddressRepository;
19
+ message: IMessageRepository;
20
+ envelope: IEnvelopeRepository;
21
+ }
@@ -0,0 +1,62 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { describe, it } from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { build } from "esbuild";
6
+
7
+ const LAMBDA_ONLY = [
8
+ /(^|\/)aws-lambda(\/|$)/,
9
+ /(^|\/)@aws-sdk\//,
10
+ /(^|\/)expect-env(\/|$)/,
11
+ /(^|\/)sqs-client(\/|$)/,
12
+ /(^|\/)logger-lambda(\/|$)/,
13
+ /(^|\/)prom-client(\/|$)/,
14
+ ];
15
+
16
+ const ENV_READERS: readonly string[] = [];
17
+
18
+ const packageDir = fileURLToPath(new URL("..", import.meta.url));
19
+
20
+ const bundleInputs = async () => {
21
+ const { metafile } = await build({
22
+ absWorkingDir: packageDir,
23
+ entryPoints: ["src/process.ts"],
24
+ bundle: true,
25
+ write: false,
26
+ metafile: true,
27
+ platform: "node",
28
+ format: "esm",
29
+ logLevel: "silent",
30
+ });
31
+ return metafile.inputs;
32
+ };
33
+
34
+ describe("the ./process entry", () => {
35
+ it("bundles without the Lambda runtime, the AWS SDK, SQS, expect-env or the Node metrics server", async () => {
36
+ const reached = Object.entries(await bundleInputs()).flatMap(
37
+ ([path, input]) => [path, ...input.imports.map((entry) => entry.path)],
38
+ );
39
+
40
+ assert.deepEqual(
41
+ reached.filter((path) =>
42
+ LAMBDA_ONLY.some((pattern) => pattern.test(path)),
43
+ ),
44
+ [],
45
+ );
46
+ });
47
+
48
+ it("reaches no workspace source that reads process.env", async () => {
49
+ const sources = Object.keys(await bundleInputs()).filter(
50
+ (path) => !path.startsWith("<") && !path.includes("node_modules/"),
51
+ );
52
+
53
+ assert.deepEqual(
54
+ sources.filter((path) =>
55
+ /\bprocess\.env\b/.test(
56
+ readFileSync(new URL(path, `file://${packageDir}`), "utf8"),
57
+ ),
58
+ ),
59
+ ENV_READERS,
60
+ );
61
+ });
62
+ });
package/src/process.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type {
2
+ AppendSentMessageEvent,
3
+ SendMessageEvent,
4
+ SmtpEvent,
5
+ } from "./events.js";
6
+ export type { AttachmentReader } from "./handlers/attachment-storage.js";
7
+ export {
8
+ handleSendMessage,
9
+ type SendMessageContext,
10
+ } from "./handlers/send-message.js";
11
+ export type { SmtpDataPorts } from "./ports.js";
package/src/processor.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import type { Logger } from "@remit/logger";
2
2
  import type { SmtpEvent } from "./events.js";
3
- import { handleSendMessage } from "./handlers/send-message.js";
3
+ import {
4
+ handleSendMessage,
5
+ type SendMessageContext,
6
+ } from "./handlers/send-message.js";
4
7
 
5
8
  export const processEvent = async (
6
9
  event: SmtpEvent,
7
10
  log: Logger,
11
+ ctx: SendMessageContext,
8
12
  /**
9
13
  * SQS's own delivery count for the record carrying this event (1 on first
10
14
  * delivery). Read by SEND_MESSAGE so it knows from it when this is the
@@ -12,11 +16,11 @@ export const processEvent = async (
12
16
  * record, so it can resolve retry exhaustion into a terminal outcome
13
17
  * (issue #951) instead of dead-lettering blindly.
14
18
  */
15
- receiveCount = 1,
19
+ receiveCount: number,
16
20
  ): Promise<void> => {
17
21
  switch (event.type) {
18
22
  case "SEND_MESSAGE":
19
- return handleSendMessage(event, log, receiveCount);
23
+ return handleSendMessage(event, ctx, receiveCount);
20
24
  case "PROCESS_OUTBOX":
21
25
  // Future: batch process all queued messages
22
26
  log.info(