@remit/smtp-worker 0.0.1 → 0.0.2

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/package.json CHANGED
@@ -1,40 +1,50 @@
1
1
  {
2
- "name": "@remit/smtp-worker",
3
- "version": "0.0.1",
4
- "type": "module",
5
- "main": "dist/index.js",
6
- "scripts": {
7
- "bundle": "node build.mjs",
8
- "test:typecheck": "tsgo --noEmit",
9
- "test:run": "node --env-file=../../localhost-test-unit.env --test 'src/**/*.test.ts'",
10
- "test": "npm run test:typecheck && npm run test:run"
11
- },
12
- "devDependencies": {
13
- "@aws-sdk/client-sqs": "*",
14
- "@aws-sdk/core": "*",
15
- "@remit/data-ports": "*",
16
- "@remit/drizzle-service": "*",
17
- "@remit/domain-enums": "*",
18
- "@remit/logger-lambda": "*",
19
- "@remit/mail-oauth-service": "*",
20
- "@remit/mailbox-service": "*",
21
- "@remit/secrets-service": "*",
22
- "@remit/smtp-service": "*",
23
- "@remit/sqs-client": "*",
24
- "@remit/storage-service": "*",
25
- "@types/aws-lambda": "*",
26
- "@types/pg": "^8.0.0",
27
- "esbuild": "^0.27.7",
28
- "expect-env": "*",
29
- "p-map": "*"
30
- },
31
- "license": "MIT",
32
- "publishConfig": {
33
- "access": "public"
34
- },
35
- "repository": {
36
- "type": "git",
37
- "url": "git+https://github.com/remit-mail/remit.git",
38
- "directory": "packages/smtp-worker"
39
- }
2
+ "name": "@remit/smtp-worker",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ },
11
+ "./data-ports": {
12
+ "types": "./src/data-ports.ts",
13
+ "default": "./src/data-ports.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "bundle": "node build.mjs",
18
+ "test:typecheck": "tsgo --noEmit",
19
+ "test:run": "node --env-file=../../localhost-test-unit.env --test 'src/**/*.test.ts'",
20
+ "test": "npm run test:typecheck && npm run test:run"
21
+ },
22
+ "devDependencies": {
23
+ "@aws-sdk/client-sqs": "*",
24
+ "@aws-sdk/core": "*",
25
+ "@remit/data-ports": "*",
26
+ "@remit/drizzle-service": "*",
27
+ "@remit/domain-enums": "*",
28
+ "@remit/logger-lambda": "*",
29
+ "@remit/mail-oauth-service": "*",
30
+ "@remit/mailbox-service": "*",
31
+ "@remit/secrets-service": "*",
32
+ "@remit/smtp-service": "*",
33
+ "@remit/sqs-client": "*",
34
+ "@remit/storage-service": "*",
35
+ "@types/aws-lambda": "*",
36
+ "@types/pg": "^8.0.0",
37
+ "esbuild": "^0.27.7",
38
+ "expect-env": "*",
39
+ "p-map": "*"
40
+ },
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/remit-mail/remit.git",
48
+ "directory": "packages/smtp-worker"
49
+ }
40
50
  }
package/src/data-ports.ts CHANGED
@@ -96,31 +96,33 @@ const buildSqliteDataPorts = async (): Promise<SmtpDataPorts> => {
96
96
  };
97
97
  };
98
98
 
99
+ let injectedDataPorts: SmtpDataPorts | null = null;
100
+
101
+ /**
102
+ * Register the DynamoDB-backed SMTP data ports from the composition root. The
103
+ * DynamoDB composition lives outside this shared, open-core module and is never
104
+ * imported here. The relational backends compose in-package below and never
105
+ * touch this seam.
106
+ */
107
+ export const setSmtpDataPorts = (ports: SmtpDataPorts): void => {
108
+ injectedDataPorts = ports;
109
+ };
110
+
99
111
  /**
100
112
  * Select the SMTP data ports from the environment, mirroring the other workers'
101
113
  * `DATA_BACKEND` selection:
102
114
  *
103
115
  * - `DATA_BACKEND=postgres` → Drizzle repos over `PG_CONNECTION_URL`.
104
116
  * - `DATA_BACKEND=sqlite` → Drizzle repos over the shared `SQLITE_DB_PATH` file.
105
- * - otherwise → ElectroDB services over `DYNAMODB_TABLE_NAME` (the AWS path,
106
- * unchanged from the pre-convergence worker).
107
- *
108
- * The DynamoDB branch loads its composition through a dynamic `import()`, so the
109
- * sole importer of the closed `@remit/remit-electrodb-service`
110
- * (`./compose-dynamodb.js`) is never loaded on the relational (open-core) tree,
111
- * where that module is not shipped. esbuild still bundles it into the Lambda
112
- * because the specifier is a relative import, not an `external`.
117
+ * - otherwise → the DynamoDB ElectroDB ports the composition root injected.
113
118
  */
114
119
  export const buildDataPortsFromEnv = async (): Promise<SmtpDataPorts> => {
115
120
  if (process.env.DATA_BACKEND === "postgres") return buildPostgresDataPorts();
116
121
  if (process.env.DATA_BACKEND === "sqlite") return buildSqliteDataPorts();
117
- // `as string` stops tsgo resolving the module in the open-core tree, which
118
- // strips it; the named contract keeps the call site typed rather than `any`.
119
- type ComposeDynamoDBModule = {
120
- buildDynamoDBDataPorts: () => SmtpDataPorts;
121
- };
122
- const { buildDynamoDBDataPorts } = (await import(
123
- "./compose-dynamodb.js" as string
124
- )) as ComposeDynamoDBModule;
125
- return buildDynamoDBDataPorts();
122
+ if (!injectedDataPorts) {
123
+ throw new Error(
124
+ "no DynamoDB SMTP data ports registered — register them with setSmtpDataPorts() from your composition root",
125
+ );
126
+ }
127
+ return injectedDataPorts;
126
128
  };
@@ -11,7 +11,10 @@ import {
11
11
  type EncryptedPayload,
12
12
  serializeEncryptedPayload,
13
13
  } from "@remit/secrets-service";
14
- import { type SendResult, SmtpConnectionError } from "@remit/smtp-service";
14
+ import {
15
+ type SendResult,
16
+ SmtpConnectionError,
17
+ } from "@remit/smtp-service";
15
18
  import type { SendMessageEvent } from "../events.js";
16
19
  import { type SendMessageDeps, sendMessage } from "./send-message-core.js";
17
20
 
@@ -23,11 +23,19 @@ import {
23
23
  import { sendMail } from "@remit/smtp-service";
24
24
  import { resolveSqsCredentials } from "@remit/sqs-client";
25
25
  import { env } from "expect-env";
26
- import { buildDataPortsFromEnv } from "../data-ports.js";
26
+ import { buildDataPortsFromEnv, type SmtpDataPorts } from "../data-ports.js";
27
27
  import type { SendMessageEvent } from "../events.js";
28
28
  import { sendMessage } from "./send-message-core.js";
29
29
 
30
- const ports = await buildDataPortsFromEnv();
30
+ // The data ports are resolved lazily and cached, not at module load: the
31
+ // DynamoDB backend is injected by the composition root before the first send,
32
+ // so building at import time would race the registration.
33
+ let portsPromise: Promise<SmtpDataPorts> | null = null;
34
+ const getPorts = (): Promise<SmtpDataPorts> => {
35
+ if (!portsPromise) portsPromise = buildDataPortsFromEnv();
36
+ return portsPromise;
37
+ };
38
+
31
39
  const dataKeyProvider = createKmsDataKeyProvider(env.KMS_KEY_ID);
32
40
  const secrets = createSecretsService(dataKeyProvider);
33
41
 
@@ -48,12 +56,6 @@ const getTokenService = () => {
48
56
  return _tokenService;
49
57
  };
50
58
 
51
- const accountService = ports.account;
52
- const outboxService = ports.outboxMessage;
53
- const addressService = ports.address;
54
- const messageService = ports.message;
55
- const envelopeService = ports.envelope;
56
-
57
59
  // APPEND_SENT_MESSAGE is processed by the IMAP worker via the
58
60
  // message-management queue (see remit-imap-worker/src/emit.ts).
59
61
  const messageMgmtQueueUrl = env.SQS_QUEUE_URL_MESSAGE_MGMT;
@@ -90,7 +92,8 @@ const findMessageByHeader = async (
90
92
  messageIdHeader: string,
91
93
  ) => {
92
94
  const messageId = deriveMessageId(accountId, messageIdHeader);
93
- return messageService.get(messageId).catch((error: unknown) => {
95
+ const { message } = await getPorts();
96
+ return message.get(messageId).catch((error: unknown) => {
94
97
  if ((error as { name?: string })?.name === "NotFoundError") return null;
95
98
  throw error;
96
99
  });
@@ -99,7 +102,8 @@ const findMessageByHeader = async (
99
102
  const getEnvelopeFromEmail = async (
100
103
  messageId: string,
101
104
  ): Promise<string | null> => {
102
- const data = await envelopeService
105
+ const { envelope } = await getPorts();
106
+ const data = await envelope
103
107
  .getMessageData(messageId)
104
108
  .catch((error: unknown) => {
105
109
  if ((error as { name?: string })?.name === "NotFoundError") return null;
@@ -122,7 +126,8 @@ const buildCredentialDeps = (): AccountCredentialsDeps => ({
122
126
  secrets,
123
127
  tokenService: getTokenService(),
124
128
  persistRotatedToken: async (accountId, encryptedHash, updatedAt) => {
125
- await accountService.update(accountId, {
129
+ const { account } = await getPorts();
130
+ await account.update(accountId, {
126
131
  oauthRefreshTokenHash: encryptedHash,
127
132
  oauthTokenUpdatedAt: updatedAt,
128
133
  });
@@ -135,19 +140,25 @@ export const handleSendMessage = (
135
140
  ): Promise<void> => {
136
141
  const credentialDeps = buildCredentialDeps();
137
142
  return sendMessage(event, log, {
138
- getOutbox: (accountConfigId, id) => outboxService.get(accountConfigId, id),
139
- getAccount: (id) => accountService.get(id),
140
- updateOutbox: (accountConfigId, id, patch) =>
141
- outboxService.update(accountConfigId, id, patch),
142
- updateOutboxStatus: (accountConfigId, id, status) =>
143
- outboxService.updateStatus(accountConfigId, id, status),
144
- markOutboxSent: (accountConfigId, id, fields) =>
145
- outboxService.markSent(accountConfigId, id, fields),
143
+ getOutbox: async (accountConfigId, id) =>
144
+ (await getPorts()).outboxMessage.get(accountConfigId, id),
145
+ getAccount: async (id) => (await getPorts()).account.get(id),
146
+ updateOutbox: async (accountConfigId, id, patch) =>
147
+ (await getPorts()).outboxMessage.update(accountConfigId, id, patch),
148
+ updateOutboxStatus: async (accountConfigId, id, status) =>
149
+ (await getPorts()).outboxMessage.updateStatus(
150
+ accountConfigId,
151
+ id,
152
+ status,
153
+ ),
154
+ markOutboxSent: async (accountConfigId, id, fields) =>
155
+ (await getPorts()).outboxMessage.markSent(accountConfigId, id, fields),
146
156
  secrets,
147
157
  resolveCredentials: (account) =>
148
158
  resolveConnectionCredentials(account, credentialDeps),
149
159
  updateConnectionState: async (id, state) => {
150
- await accountService.update(id, {
160
+ const { account } = await getPorts();
161
+ await account.update(id, {
151
162
  connectionState:
152
163
  state as (typeof ConnectionState)[keyof typeof ConnectionState],
153
164
  });
@@ -156,10 +167,18 @@ export const handleSendMessage = (
156
167
  emitAppendSentMessage,
157
168
  engagement: {
158
169
  resolveAddressId: deriveAddressId,
159
- incrementOutboundCount: (accountConfigId, addressId, now) =>
160
- addressService.incrementOutboundCount(accountConfigId, addressId, now),
161
- incrementReplyCount: (accountConfigId, addressId, now) =>
162
- addressService.incrementReplyCount(accountConfigId, addressId, now),
170
+ incrementOutboundCount: async (accountConfigId, addressId, now) =>
171
+ (await getPorts()).address.incrementOutboundCount(
172
+ accountConfigId,
173
+ addressId,
174
+ now,
175
+ ),
176
+ incrementReplyCount: async (accountConfigId, addressId, now) =>
177
+ (await getPorts()).address.incrementReplyCount(
178
+ accountConfigId,
179
+ addressId,
180
+ now,
181
+ ),
163
182
  findMessageByHeader,
164
183
  getEnvelopeFromEmail,
165
184
  },