@remit/account-worker 0.0.1 → 0.0.3

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,50 +1,74 @@
1
1
  {
2
- "name": "@remit/account-worker",
3
- "version": "0.0.1",
4
- "type": "module",
5
- "main": "dist/index.js",
6
- "scripts": {
7
- "bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
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-cloudfront": "*",
14
- "@aws-sdk/client-cognito-identity-provider": "*",
15
- "@aws-sdk/client-dynamodb": "*",
16
- "@aws-sdk/client-s3": "*",
17
- "@aws-sdk/client-sqs": "*",
18
- "@aws-sdk/core": "*",
19
- "@aws-sdk/lib-dynamodb": "*",
20
- "@aws-sdk/lib-storage": "*",
21
- "@aws-sdk/s3-request-presigner": "*",
22
- "@remit/backend": "*",
23
- "@remit/data-ports": "*",
24
- "@remit/electrodb-entities": "*",
25
- "@remit/drizzle-service": "*",
26
- "@remit/domain-enums": "*",
27
- "@remit/logger-lambda": "*",
28
- "@remit/search-index-worker": "*",
29
- "@remit/search-service": "*",
30
- "@remit/sqs-client": "*",
31
- "@remit/storage-service": "*",
32
- "@types/archiver": "*",
33
- "@types/aws-lambda": "*",
34
- "archiver": "*",
35
- "aws-sdk-client-mock": "*",
36
- "electrodb": "*",
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/account-worker"
49
- }
2
+ "name": "@remit/account-worker",
3
+ "version": "0.0.3",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ },
11
+ "./cascade": {
12
+ "types": "./src/cascade.ts",
13
+ "default": "./src/cascade.ts"
14
+ },
15
+ "./deletion-capabilities": {
16
+ "types": "./src/deletion-capabilities.ts",
17
+ "default": "./src/deletion-capabilities.ts"
18
+ },
19
+ "./fanout": {
20
+ "types": "./src/handlers/account-fanout.ts",
21
+ "default": "./src/handlers/account-fanout.ts"
22
+ },
23
+ "./finalize": {
24
+ "types": "./src/handlers/account-finalize.ts",
25
+ "default": "./src/handlers/account-finalize.ts"
26
+ }
27
+ },
28
+ "scripts": {
29
+ "bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
30
+ "test:typecheck": "tsgo --noEmit",
31
+ "test:run": "node --env-file=../../localhost-test-unit.env --test 'src/**/*.test.ts'",
32
+ "test": "npm run test:typecheck && npm run test:run"
33
+ },
34
+ "dependencies": {
35
+ "@remit/data-ports": "*",
36
+ "@remit/logger-lambda": "*",
37
+ "@remit/drizzle-service": "*",
38
+ "@remit/backend": "*",
39
+ "@remit/sqs-client": "*",
40
+ "@remit/storage-service": "*",
41
+ "@remit/electrodb-entities": "*",
42
+ "@remit/search-index-worker": "*"
43
+ },
44
+ "devDependencies": {
45
+ "@aws-sdk/client-cloudfront": "*",
46
+ "@aws-sdk/client-cognito-identity-provider": "*",
47
+ "@aws-sdk/client-dynamodb": "*",
48
+ "@aws-sdk/client-s3": "*",
49
+ "@aws-sdk/client-sqs": "*",
50
+ "@aws-sdk/core": "*",
51
+ "@aws-sdk/lib-dynamodb": "*",
52
+ "@aws-sdk/lib-storage": "*",
53
+ "@aws-sdk/s3-request-presigner": "*",
54
+ "@remit/domain-enums": "*",
55
+ "@remit/search-service": "*",
56
+ "@types/archiver": "*",
57
+ "@types/aws-lambda": "*",
58
+ "archiver": "*",
59
+ "aws-sdk-client-mock": "*",
60
+ "electrodb": "*",
61
+ "esbuild": "^0.27.7",
62
+ "expect-env": "*",
63
+ "p-map": "*"
64
+ },
65
+ "license": "MIT",
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/remit-mail/remit.git",
72
+ "directory": "packages/account-worker"
73
+ }
50
74
  }
package/src/config.ts CHANGED
@@ -6,15 +6,13 @@ import { createStorageService } from "@remit/storage-service/s3";
6
6
  import { env } from "expect-env";
7
7
  import type { CascadeServices } from "./cascade.js";
8
8
 
9
- const remitClient = await getClient();
10
-
11
9
  export const sqsClient = new SQSClient({
12
10
  credentials: resolveSqsCredentials(),
13
11
  });
14
12
 
15
13
  // SQS env vars are lazy-evaluated. The fanout worker needs them at handler time;
16
- // the finalize worker imports `cascadeServices` from this module but talks to no
17
- // queue, so its Lambda doesn't carry the env vars. Eager evaluation here would
14
+ // the finalize worker imports `getCascadeServices` from this module but talks to
15
+ // no queue, so its Lambda doesn't carry the env vars. Eager evaluation here would
18
16
  // crash finalize at module load — getters defer the read to the fanout-only call
19
17
  // sites.
20
18
  export const getSearchIndexQueueUrl = (): string =>
@@ -31,34 +29,42 @@ export const graceSeconds = graceSecondsRaw
31
29
  ? Number.parseInt(graceSecondsRaw, 10)
32
30
  : 60;
33
31
 
32
+ // The RemitClient is resolved lazily and cached, not at module load: the
33
+ // DynamoDB backend is injected by the composition root before the first cascade
34
+ // runs, so building at import time would race the registration.
35
+ let cascadeServicesPromise: Promise<CascadeServices> | null = null;
36
+
34
37
  // Every cascade service is a `RemitClient` repository, so the whole enumeration
35
38
  // runs on whatever backend `getClient()` selected — DynamoDB (ElectroDB),
36
39
  // Postgres, or SQLite (Drizzle). Filter/FilterAnchor/Label/MessageLabel (Smart
37
40
  // Organize, RFC 034) are present on all three backends via the client, so they
38
41
  // need no backend-specific wiring here.
39
- export const cascadeServices: CascadeServices = {
40
- accountConfigService: remitClient.accountConfig,
41
- accountService: remitClient.account,
42
- addressService: remitClient.address,
43
- mailboxService: remitClient.mailbox,
44
- messageService: remitClient.message,
45
- messageFlagService: remitClient.messageFlag,
46
- envelopeService: remitClient.envelope,
47
- outboxMessageService: remitClient.outboxMessage,
48
- threadMessageService: remitClient.threadMessage,
49
- mailboxLockService: remitClient.mailboxLock,
50
- messagePlacementMoveService: remitClient.placementMove,
51
- messageFlagPushService: remitClient.flagPush,
52
- accountExportRequestService: remitClient.accountExportRequest,
53
- accountSettingService: remitClient.accountSetting,
54
- filterService: remitClient.filter,
55
- filterAnchorService: remitClient.filterAnchor,
56
- labelService: remitClient.label,
57
- messageLabelService: remitClient.messageLabel,
42
+ export const getCascadeServices = (): Promise<CascadeServices> => {
43
+ if (!cascadeServicesPromise) {
44
+ cascadeServicesPromise = getClient().then((remitClient) => ({
45
+ accountConfigService: remitClient.accountConfig,
46
+ accountService: remitClient.account,
47
+ addressService: remitClient.address,
48
+ mailboxService: remitClient.mailbox,
49
+ messageService: remitClient.message,
50
+ messageFlagService: remitClient.messageFlag,
51
+ envelopeService: remitClient.envelope,
52
+ outboxMessageService: remitClient.outboxMessage,
53
+ threadMessageService: remitClient.threadMessage,
54
+ mailboxLockService: remitClient.mailboxLock,
55
+ messagePlacementMoveService: remitClient.placementMove,
56
+ messageFlagPushService: remitClient.flagPush,
57
+ accountExportRequestService: remitClient.accountExportRequest,
58
+ accountSettingService: remitClient.accountSetting,
59
+ filterService: remitClient.filter,
60
+ filterAnchorService: remitClient.filterAnchor,
61
+ labelService: remitClient.label,
62
+ messageLabelService: remitClient.messageLabel,
63
+ }));
64
+ }
65
+ return cascadeServicesPromise;
58
66
  };
59
67
 
60
- export const accountConfigService = cascadeServices.accountConfigService;
61
-
62
68
  let storageService: StorageService | null = null;
63
69
  export const getStorageService = (): StorageService => {
64
70
  if (!storageService) {
@@ -6,15 +6,16 @@ import type { CascadeEntity } from "./cascade.js";
6
6
  * else — enumerating the AccountConfig's rows, the FIFO purge orchestration, the
7
7
  * queue sends — is backend-neutral and lives in the handlers unchanged.
8
8
  *
9
- * Two composition roots build this, selected by `DATA_BACKEND` exactly like
10
- * `@remit/backend`'s `compose-{dynamodb,postgres,sqlite}`:
9
+ * Two composition roots build this:
11
10
  *
12
- * - `compose-dynamodb.ts` (AWS custody stack): Cognito global sign-out,
11
+ * - The DynamoDB composition (AWS custody stack): Cognito global sign-out,
13
12
  * CloudFront invalidation, raw-S3 prefix delete, DynamoDB `BatchWriteItem`
14
- * cascade. Byte-for-byte the pre-split behavior. Stripped from the open-core
15
- * export and loaded through a lazy `import()` so the closed AWS SDK/ElectroDB
16
- * graph never reaches the relational tree.
17
- * - `compose-relational.ts` (self-host stack, RFC 035/036): no-op sign-out
13
+ * cascade. Byte-for-byte the pre-split behavior. It imports the AWS SDK /
14
+ * ElectroDB graph and lives outside this shared, open-core module; it is
15
+ * injected here through `setDeletionCapabilities` so that graph never reaches
16
+ * the relational tree.
17
+ * - `compose-relational.ts` (self-host stack, RFC 035/036), selected by
18
+ * `DATA_BACKEND`: no-op sign-out
18
19
  * (deleting the AccountConfig severs the session's data resolution; there is
19
20
  * no CDN to sign out of), no-op invalidation (Caddy proxies `/content/*`
20
21
  * straight to the backend with no cache), filesystem prefix delete, and the
@@ -60,6 +61,20 @@ const isRelationalBackend = (): boolean => {
60
61
  return backend === "postgres" || backend === "sqlite";
61
62
  };
62
63
 
64
+ let injectedCapabilities: DeletionCapabilities | null = null;
65
+
66
+ /**
67
+ * Register the DynamoDB-backed deletion capabilities from the composition root.
68
+ * The DynamoDB composition imports the AWS SDK / ElectroDB graph and lives
69
+ * outside this shared, open-core module; it is never imported here. The
70
+ * relational backend composes in-package below and never touches this seam.
71
+ */
72
+ export const setDeletionCapabilities = (
73
+ capabilities: DeletionCapabilities,
74
+ ): void => {
75
+ injectedCapabilities = capabilities;
76
+ };
77
+
63
78
  const buildDeletionCapabilitiesFromEnv =
64
79
  async (): Promise<DeletionCapabilities> => {
65
80
  if (isRelationalBackend()) {
@@ -68,15 +83,12 @@ const buildDeletionCapabilitiesFromEnv =
68
83
  );
69
84
  return buildRelationalDeletionCapabilities();
70
85
  }
71
- // `as string` stops tsgo resolving the module in the open-core tree, which
72
- // strips it; the named contract keeps the call site typed rather than `any`.
73
- type ComposeDynamoDBModule = {
74
- buildDynamoDBDeletionCapabilities: () => DeletionCapabilities;
75
- };
76
- const { buildDynamoDBDeletionCapabilities } = (await import(
77
- "./compose-dynamodb.js" as string
78
- )) as ComposeDynamoDBModule;
79
- return buildDynamoDBDeletionCapabilities();
86
+ if (!injectedCapabilities) {
87
+ throw new Error(
88
+ "no DynamoDB deletion capabilities registered — register them with setDeletionCapabilities() from your composition root",
89
+ );
90
+ }
91
+ return injectedCapabilities;
80
92
  };
81
93
 
82
94
  let capabilitiesPromise: Promise<DeletionCapabilities> | undefined;
@@ -3,7 +3,7 @@ import type { Logger } from "@remit/logger-lambda";
3
3
  import type { StorageService } from "@remit/storage-service";
4
4
  import { ZipArchive } from "archiver";
5
5
  import type { CascadeServices } from "../cascade.js";
6
- import { cascadeServices, getStorageService } from "../config.js";
6
+ import { getCascadeServices, getStorageService } from "../config.js";
7
7
  import type { AccountExportEvent } from "../events.js";
8
8
 
9
9
  const THIRTY_DAYS_MILLIS = 30 * 24 * 60 * 60 * 1000;
@@ -13,8 +13,8 @@ export interface ProcessAccountExportDeps {
13
13
  storageService: StorageService;
14
14
  }
15
15
 
16
- const defaultDeps = (): ProcessAccountExportDeps => ({
17
- services: cascadeServices,
16
+ const defaultDeps = async (): Promise<ProcessAccountExportDeps> => ({
17
+ services: await getCascadeServices(),
18
18
  storageService: getStorageService(),
19
19
  });
20
20
 
@@ -66,10 +66,10 @@ const appendMessageBodies = async (
66
66
  export const processAccountExport = async (
67
67
  event: AccountExportEvent,
68
68
  log: Logger,
69
- deps: ProcessAccountExportDeps = defaultDeps(),
69
+ deps?: ProcessAccountExportDeps,
70
70
  ): Promise<void> => {
71
71
  const { accountConfigId, accountExportRequestId } = event;
72
- const { services, storageService } = deps;
72
+ const { services, storageService } = deps ?? (await defaultDeps());
73
73
  const { accountExportRequestService } = services;
74
74
 
75
75
  await accountExportRequestService.get(accountExportRequestId);
@@ -1,12 +1,16 @@
1
1
  import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
2
- import { createLogger, type Logger, withTelemetry } from "@remit/logger-lambda";
2
+ import {
3
+ createLogger,
4
+ type Logger,
5
+ withTelemetry,
6
+ } from "@remit/logger-lambda";
3
7
  import type { SQSBatchResponse, SQSEvent, SQSHandler } from "aws-lambda";
4
8
  import type { CascadeServices } from "../cascade.js";
5
9
  import { enumerateCascadeEntities } from "../cascade.js";
6
10
  import {
7
- cascadeServices,
8
11
  getAccountFinalizeQueueUrl,
9
12
  getAccountPurgeDeleteQueueUrl,
13
+ getCascadeServices,
10
14
  getImapWorkerQueueUrl,
11
15
  sqsClient,
12
16
  } from "../config.js";
@@ -44,7 +48,7 @@ export const processAccountFanout = async (
44
48
  log: Logger,
45
49
  deps: ProcessAccountFanoutDeps = {},
46
50
  ): Promise<void> => {
47
- const services = deps.services ?? cascadeServices;
51
+ const services = deps.services ?? (await getCascadeServices());
48
52
  const sqs = deps.sqs ?? sqsClient;
49
53
 
50
54
  if (event.type === "AccountDataPurge") {
@@ -1,5 +1,9 @@
1
1
  import { inspect } from "node:util";
2
- import { createLogger, type Logger, withTelemetry } from "@remit/logger-lambda";
2
+ import {
3
+ createLogger,
4
+ type Logger,
5
+ withTelemetry,
6
+ } from "@remit/logger-lambda";
3
7
  import type { SQSEvent, SQSHandler } from "aws-lambda";
4
8
  import {
5
9
  type CascadeEntity,
@@ -7,7 +11,7 @@ import {
7
11
  collectMessageChildEntities,
8
12
  enumerateCascadeEntities,
9
13
  } from "../cascade.js";
10
- import { cascadeServices as defaultCascadeServices } from "../config.js";
14
+ import { getCascadeServices } from "../config.js";
11
15
  import {
12
16
  type DeletionCapabilities,
13
17
  getDeletionCapabilities,
@@ -30,7 +34,7 @@ const resolveDeps = async (
30
34
  services: CascadeServices;
31
35
  }> => ({
32
36
  capabilities: deps.capabilities ?? (await getDeletionCapabilities()),
33
- services: deps.cascadeServices ?? defaultCascadeServices,
37
+ services: deps.cascadeServices ?? (await getCascadeServices()),
34
38
  });
35
39
 
36
40
  /**
@@ -10,10 +10,10 @@ import type { SearchIndexMessage } from "@remit/search-index-worker";
10
10
  import { Entity } from "electrodb";
11
11
  import type { CascadeServices } from "../cascade.js";
12
12
  import {
13
- cascadeServices as defaultCascadeServices,
14
13
  dataBackend as defaultDataBackend,
15
14
  sqsClient as defaultSqsClient,
16
15
  getAccountPurgeDeleteQueueUrl,
16
+ getCascadeServices,
17
17
  getSearchIndexQueueUrl,
18
18
  } from "../config.js";
19
19
  import type {
@@ -161,7 +161,7 @@ export const processAccountDataPurge = async (
161
161
  deps: ProcessPurgeFanoutDeps = {},
162
162
  ): Promise<void> => {
163
163
  const { accountId, accountConfigId } = event;
164
- const services = deps.services ?? defaultCascadeServices;
164
+ const services = deps.services ?? (await getCascadeServices());
165
165
  const sqs = deps.sqs ?? defaultSqsClient;
166
166
  const accountPurgeDeleteQueueUrl =
167
167
  deps.accountPurgeDeleteQueueUrl ?? getAccountPurgeDeleteQueueUrl();