@remit/backend 0.0.50 → 0.0.52

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 (44) hide show
  1. package/dev-server/content-auth.test.ts +9 -9
  2. package/dev-server/content-auth.ts +2 -2
  3. package/dev-server/relational-health.test.ts +0 -8
  4. package/dev-server/relational-health.ts +3 -21
  5. package/dev-server/server.ts +7 -9
  6. package/package.json +4 -7
  7. package/scripts/backfill-list-id.ts +1 -1
  8. package/src/data-backend.test.ts +1 -3
  9. package/src/data-backend.ts +8 -14
  10. package/src/derive/contentSignature.test.ts +1 -19
  11. package/src/derive/contentSignature.ts +10 -10
  12. package/src/derive/contentUrl.ts +1 -1
  13. package/src/handlers/account-oauth.ts +1 -1
  14. package/src/handlers/account.ts +1 -1
  15. package/src/handlers/address.ts +1 -1
  16. package/src/handlers/config.ts +1 -1
  17. package/src/handlers/filter.ts +1 -1
  18. package/src/handlers/folder-role.ts +1 -1
  19. package/src/handlers/label.ts +1 -1
  20. package/src/handlers/mailbox.ts +3 -3
  21. package/src/handlers/me.test.ts +1 -1
  22. package/src/handlers/me.ts +1 -1
  23. package/src/handlers/message.ts +1 -1
  24. package/src/handlers/organize.test.ts +20 -0
  25. package/src/handlers/organize.ts +1 -1
  26. package/src/handlers/outbox.ts +1 -1
  27. package/src/handlers/search.ts +1 -1
  28. package/src/handlers/sync.ts +1 -1
  29. package/src/handlers/thread.ts +1 -1
  30. package/src/handlers/unified-threads.ts +1 -1
  31. package/src/index.ts +2 -2
  32. package/src/jwt-auth.test.ts +6 -6
  33. package/src/jwt-auth.ts +2 -2
  34. package/src/service/compose-sqlite.ts +8 -14
  35. package/src/service/create-remit-client.ts +6 -6
  36. package/src/service/data-client.test.ts +96 -0
  37. package/src/service/data-client.ts +58 -0
  38. package/src/service/organize.test.ts +9 -4
  39. package/src/service/organize.ts +5 -7
  40. package/src/service/semantic-capability.test.ts +10 -16
  41. package/src/service/semantic-capability.ts +1 -1
  42. package/src/service/compose-postgres.ts +0 -76
  43. package/src/service/dynamodb.test.ts +0 -98
  44. package/src/service/dynamodb.ts +0 -57
@@ -8,7 +8,7 @@ const PATH = "accounts/cfg-alice/acc-alice/messages/msg-1/parts/1.2";
8
8
  const now = () => Math.floor(Date.now() / 1000);
9
9
 
10
10
  describe("authorizeContentRequest", () => {
11
- it("is a no-op outside Postgres mode (AWS serves unsigned URLs)", () => {
11
+ it("is a no-op on the AWS path (unsigned URLs)", () => {
12
12
  const result = authorizeContentRequest({
13
13
  dataBackend: undefined,
14
14
  secret: undefined,
@@ -20,10 +20,10 @@ describe("authorizeContentRequest", () => {
20
20
  assert.deepEqual(result, { authorized: true });
21
21
  });
22
22
 
23
- it("authorizes a validly signed request in Postgres mode", () => {
23
+ it("authorizes a validly signed request on the self-host stack", () => {
24
24
  const { exp, sig } = createContentSigner(SECRET)(PATH);
25
25
  const result = authorizeContentRequest({
26
- dataBackend: "postgres",
26
+ dataBackend: "sqlite",
27
27
  secret: SECRET,
28
28
  relativePath: PATH,
29
29
  exp: String(exp),
@@ -35,7 +35,7 @@ describe("authorizeContentRequest", () => {
35
35
 
36
36
  it("returns 401 when the signature is absent", () => {
37
37
  const result = authorizeContentRequest({
38
- dataBackend: "postgres",
38
+ dataBackend: "sqlite",
39
39
  secret: SECRET,
40
40
  relativePath: PATH,
41
41
  exp: undefined,
@@ -49,7 +49,7 @@ describe("authorizeContentRequest", () => {
49
49
  it("returns 403 for a signature minted for another account's path", () => {
50
50
  const { exp, sig } = createContentSigner(SECRET)(PATH);
51
51
  const result = authorizeContentRequest({
52
- dataBackend: "postgres",
52
+ dataBackend: "sqlite",
53
53
  secret: SECRET,
54
54
  relativePath: "accounts/cfg-bob/acc-bob/messages/msg-9/parts/1.2",
55
55
  exp: String(exp),
@@ -63,7 +63,7 @@ describe("authorizeContentRequest", () => {
63
63
  it("returns 403 for an expired signature", () => {
64
64
  const { exp, sig } = createContentSigner(SECRET, -20)(PATH);
65
65
  const result = authorizeContentRequest({
66
- dataBackend: "postgres",
66
+ dataBackend: "sqlite",
67
67
  secret: SECRET,
68
68
  relativePath: PATH,
69
69
  exp: String(exp),
@@ -74,9 +74,9 @@ describe("authorizeContentRequest", () => {
74
74
  assert.equal(result.authorized === false && result.status, 403);
75
75
  });
76
76
 
77
- it("fails closed with 500 in Postgres mode when no signing secret is configured", () => {
77
+ it("fails closed with 500 when no signing secret is configured", () => {
78
78
  const result = authorizeContentRequest({
79
- dataBackend: "postgres",
79
+ dataBackend: "sqlite",
80
80
  secret: undefined,
81
81
  relativePath: PATH,
82
82
  exp: "123",
@@ -87,7 +87,7 @@ describe("authorizeContentRequest", () => {
87
87
  assert.equal(result.authorized === false && result.status, 500);
88
88
  });
89
89
 
90
- it("enforces signatures in SQLite mode the same as Postgres", () => {
90
+ it("enforces signatures on every self-host request", () => {
91
91
  const { exp, sig } = createContentSigner(SECRET)(PATH);
92
92
  const valid = authorizeContentRequest({
93
93
  dataBackend: "sqlite",
@@ -2,7 +2,7 @@ import { verifyContentSignature } from "../src/derive/contentSignature.js";
2
2
 
3
3
  /**
4
4
  * Decide whether a `/content` request is authorized. Enforcement applies on the
5
- * self-host SQL backends (postgres and sqlite), where this server is the
5
+ * self-host SQL backend (sqlite), where this server is the
6
6
  * deployed backend container and content URLs are signed. On AWS-local dev
7
7
  * (`DATA_BACKEND` unset) `/content` is served straight from the filesystem
8
8
  * stand-in for CloudFront and URLs are unsigned, so the check is a no-op.
@@ -25,7 +25,7 @@ export const authorizeContentRequest = (
25
25
  ):
26
26
  | { authorized: true }
27
27
  | { authorized: false; status: number; reason: string } => {
28
- if (input.dataBackend !== "postgres" && input.dataBackend !== "sqlite") {
28
+ if (input.dataBackend !== "sqlite") {
29
29
  return { authorized: true };
30
30
  }
31
31
 
@@ -80,12 +80,4 @@ describe("checkRelationalStore", () => {
80
80
  chmodSync(dbPath, 0o400);
81
81
  assert.equal(await checkRelationalStore(), false);
82
82
  });
83
-
84
- it("is false when Postgres connection fails", async () => {
85
- process.env.DATA_BACKEND = "postgres";
86
- process.env.PG_CONNECTION_URL =
87
- "postgresql://remit:remit@127.0.0.1:1/nonexistent";
88
-
89
- assert.equal(await checkRelationalStore(), false);
90
- });
91
83
  });
@@ -21,23 +21,9 @@ const pingSqlite = async (): Promise<void> => {
21
21
  }
22
22
  };
23
23
 
24
- const pingPostgres = async (): Promise<void> => {
25
- const { Client } = await import("pg");
26
- const client = new Client({
27
- connectionString: process.env.PG_CONNECTION_URL,
28
- connectionTimeoutMillis: 3000,
29
- });
30
- await client.connect();
31
- try {
32
- await client.query("SELECT 1");
33
- } finally {
34
- await client.end();
35
- }
36
- };
37
-
38
24
  /**
39
- * A trivial read against whichever relational store DATA_BACKEND selects the
40
- * dependency `/health` must actually exercise (RFC 037 D5, R9) so a backend that
25
+ * A trivial read against the relational store the dependency `/health` must
26
+ * actually exercise (RFC 037 D5, R9) so a backend that
41
27
  * booted with an unusable database reports unhealthy instead of a bare 200. The
42
28
  * AWS/DynamoDB path has no relational store and is never checked.
43
29
  */
@@ -45,11 +31,7 @@ export const checkRelationalStore = async (): Promise<boolean> => {
45
31
  if (!isSelfHostSqlBackend()) return true;
46
32
 
47
33
  try {
48
- if (process.env.DATA_BACKEND === "sqlite") {
49
- await pingSqlite();
50
- } else {
51
- await pingPostgres();
52
- }
34
+ await pingSqlite();
53
35
  return true;
54
36
  } catch {
55
37
  return false;
@@ -17,7 +17,7 @@ import express, {
17
17
  } from "express";
18
18
  import { handler, OpenAPISpec } from "../src/index.js";
19
19
  import { safeJsonParse } from "../src/json.js";
20
- import { getClient } from "../src/service/dynamodb.js";
20
+ import { getClient } from "../src/service/data-client.js";
21
21
  import { authorizeContentRequest } from "./content-auth.js";
22
22
  import { serveContent } from "./content-handler.js";
23
23
  import { resolveContentPath } from "./content-path.js";
@@ -28,15 +28,13 @@ import { collectAccountSyncAges } from "./sync-age.js";
28
28
 
29
29
  const app = express();
30
30
 
31
- // The self-host relational backends Postgres and, from RFC 036, SQLite — both
32
- // run better-auth and the APISIX edge; the AWS-local (DynamoDB) dev path runs
33
- // neither. Everything gated on "not the AWS-local path" keys off this.
34
- const isSelfHostBackend =
35
- process.env.DATA_BACKEND === "postgres" ||
36
- process.env.DATA_BACKEND === "sqlite";
31
+ // The self-host relational backend runs better-auth and the APISIX edge; the
32
+ // AWS-local (DynamoDB) dev path runs neither. Everything gated on "not the
33
+ // AWS-local path" keys off this.
34
+ const isSelfHostBackend = process.env.DATA_BACKEND === "sqlite";
37
35
 
38
36
  // CORS is driven by CORS_ALLOWED_ORIGINS (comma-separated, or `*`). On the
39
- // self-host backends it is required config — refuse to start if unset, so the
37
+ // self-host backend it is required config — refuse to start if unset, so the
40
38
  // deployed edge is never accidentally wide open by omission. On the AWS-local
41
39
  // dev path it defaults to `*` to keep the existing local flow working.
42
40
  const configuredCorsOrigins = parseAllowedOrigins(
@@ -218,7 +216,7 @@ const STORAGE_BASE = resolve(
218
216
  app.get(/^\/content\/.+$/, async (req: Request, res: Response) => {
219
217
  const storageKey = req.path.replace(/^\/content\//, "");
220
218
 
221
- // On the Postgres stack this route is the deployed content-delivery surface;
219
+ // On the self-host stack this route is the deployed content-delivery surface;
222
220
  // require a valid signed URL (HMAC + expiry, scoped to the owning account)
223
221
  // before touching storage. Bearer auth can't ride on an `<img src>` content
224
222
  // load, so the signature carried in the query string is the authorization.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -12,8 +12,8 @@
12
12
  "default": "./src/index.ts"
13
13
  },
14
14
  "./client": {
15
- "types": "./src/service/dynamodb.ts",
16
- "default": "./src/service/dynamodb.ts"
15
+ "types": "./src/service/data-client.ts",
16
+ "default": "./src/service/data-client.ts"
17
17
  },
18
18
  "./create-remit-client": {
19
19
  "types": "./src/service/create-remit-client.ts",
@@ -73,14 +73,11 @@
73
73
  "@aws-sdk/client-secrets-manager": "*",
74
74
  "@aws-sdk/client-sqs": "*",
75
75
  "better-sqlite3": "^12.11.1",
76
- "drizzle-orm": "^0.45.2",
77
76
  "expect-env": "*",
78
77
  "openapi-backend": "*",
79
78
  "p-map": "*",
80
- "pg": "^8.0.0",
81
79
  "@types/aws-lambda": "*",
82
- "@types/better-sqlite3": "^7.6.13",
83
- "@types/pg": "^8.0.0"
80
+ "@types/better-sqlite3": "^7.6.13"
84
81
  },
85
82
  "publishConfig": {
86
83
  "access": "public"
@@ -7,7 +7,7 @@ import {
7
7
  type ListIdBackfillCheckpoint,
8
8
  type ListIdBackfillCheckpointStore,
9
9
  } from "@remit/mailbox-service";
10
- import { getClient } from "../src/service/dynamodb.js";
10
+ import { getClient } from "../src/service/data-client.js";
11
11
 
12
12
  /**
13
13
  * One-time, full-corpus `ThreadMessage.listId` backfill (issue #263). Ships as
@@ -9,9 +9,7 @@ describe("usesBetterAuthJwt", () => {
9
9
  else process.env.DATA_BACKEND = ORIGINAL;
10
10
  });
11
11
 
12
- it("is true for the self-host SQL backends", () => {
13
- process.env.DATA_BACKEND = "postgres";
14
- assert.equal(usesBetterAuthJwt(), true);
12
+ it("is true for the self-host SQL backend", () => {
15
13
  process.env.DATA_BACKEND = "sqlite";
16
14
  assert.equal(usesBetterAuthJwt(), true);
17
15
  });
@@ -1,20 +1,14 @@
1
1
  /**
2
- * The two self-host SQL backends (RFC 034/035/036), as opposed to the AWS
3
- * DynamoDB path.
2
+ * The self-host SQL backend (RFC 034/035/036), as opposed to the AWS DynamoDB
3
+ * path.
4
4
  */
5
- export const isSelfHostSqlBackend = (): boolean => {
6
- const backend = process.env.DATA_BACKEND;
7
- return backend === "postgres" || backend === "sqlite";
8
- };
5
+ export const isSelfHostSqlBackend = (): boolean =>
6
+ process.env.DATA_BACKEND === "sqlite";
9
7
 
10
8
  /**
11
- * The self-host SQL backends authenticate requests and sign content URLs the
12
- * same way: a better-auth RS256 JWT verified at the edge and re-verified
13
- * in-process, no Cognito authorizer. DynamoDB is the AWS path (Cognito claims,
14
- * Lambda@Edge content guard) and is deliberately excluded.
15
- *
16
- * Guarding these paths on `=== "postgres"` alone left the sqlite deployment
17
- * with no claim injection (every identity-bound request 500s) and unsigned
18
- * `/content/*` URLs; both branch on this predicate instead.
9
+ * The self-host SQL backend authenticates requests and signs content URLs with
10
+ * a better-auth RS256 JWT verified at the edge and re-verified in-process, no
11
+ * Cognito authorizer. DynamoDB is the AWS path (Cognito claims, Lambda@Edge
12
+ * content guard) and is deliberately excluded.
19
13
  */
20
14
  export const usesBetterAuthJwt = (): boolean => isSelfHostSqlBackend();
@@ -110,29 +110,11 @@ describe("getContentSigner", () => {
110
110
  else process.env.BETTER_AUTH_SECRET = ORIGINAL_SECRET;
111
111
  });
112
112
 
113
- it("returns undefined outside Postgres mode (AWS keeps unsigned URLs)", () => {
113
+ it("returns undefined on the AWS path (unsigned URLs)", () => {
114
114
  delete process.env.DATA_BACKEND;
115
115
  assert.equal(getContentSigner(), undefined);
116
116
  });
117
117
 
118
- it("returns a working signer in Postgres mode", () => {
119
- process.env.DATA_BACKEND = "postgres";
120
- process.env.BETTER_AUTH_SECRET = SECRET;
121
- const signer = getContentSigner();
122
- assert.ok(signer);
123
- const { exp, sig } = signer(PATH_A);
124
- assert.deepEqual(
125
- verifyContentSignature(PATH_A, String(exp), sig, SECRET, nowSeconds()),
126
- { valid: true },
127
- );
128
- });
129
-
130
- it("throws in Postgres mode when the master secret is missing (fail loud)", () => {
131
- process.env.DATA_BACKEND = "postgres";
132
- delete process.env.BETTER_AUTH_SECRET;
133
- assert.throws(() => getContentSigner(), /BETTER_AUTH_SECRET/);
134
- });
135
-
136
118
  it("returns a working signer in SQLite mode", () => {
137
119
  process.env.DATA_BACKEND = "sqlite";
138
120
  process.env.BETTER_AUTH_SECRET = SECRET;
@@ -2,10 +2,10 @@ import { createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { usesBetterAuthJwt } from "../data-backend.js";
3
3
 
4
4
  /**
5
- * Signed-URL scheme for `/content/*` on the Postgres stack.
5
+ * Signed-URL scheme for `/content/*` on the self-host stack.
6
6
  *
7
7
  * On AWS the same bytes are guarded by CloudFront + a Lambda@Edge JWT verifier.
8
- * The Postgres stack serves `/content/*` straight from the backend container,
8
+ * The self-host stack serves `/content/*` straight from the backend container,
9
9
  * and a bearer token cannot ride along on an `<img src>` / `<a href>` content
10
10
  * load rendered inside email HTML. So the backend signs each content URL when
11
11
  * it hands the SPA a `BodyPartResponse.contentUrl`, and the `/content` route
@@ -37,7 +37,7 @@ export const CONTENT_URL_TTL_SECONDS = 3600;
37
37
  * separation via a fixed label keeps the content-signing key distinct from the
38
38
  * raw better-auth secret, so a compromise of one signing space does not reveal
39
39
  * the other. Reusing `BETTER_AUTH_SECRET` as the master means no new secret has
40
- * to be provisioned on the Postgres stack — it is already required there.
40
+ * to be provisioned on the self-host stack — it is already required there.
41
41
  */
42
42
  const deriveSigningKey = (masterSecret: string): Buffer =>
43
43
  createHmac("sha256", masterSecret).update(KEY_DERIVATION_LABEL).digest();
@@ -78,19 +78,19 @@ export const createContentSigner = (
78
78
 
79
79
  /**
80
80
  * Build the content-URL signer for the current environment, or `undefined` when
81
- * signing does not apply. Signing is enforced on the self-host SQL backends
82
- * (postgres and sqlite), which serve `/content/*` straight from the backend
83
- * container; on AWS the Lambda@Edge JWT verifier guards `/content/*` and the URL
84
- * stays unsigned so CloudFront/S3 behaviour is unchanged. Throws on those
85
- * backends when the master secret is missing, so a misconfigured deploy fails
86
- * loud rather than shipping unsigned (unauthenticated) content URLs.
81
+ * signing does not apply. Signing is enforced on the self-host SQL backend,
82
+ * which serves `/content/*` straight from the backend container; on AWS the
83
+ * Lambda@Edge JWT verifier guards `/content/*` and the URL stays unsigned so
84
+ * CloudFront/S3 behaviour is unchanged. Throws on the self-host backend when the
85
+ * master secret is missing, so a misconfigured deploy fails loud rather than
86
+ * shipping unsigned (unauthenticated) content URLs.
87
87
  */
88
88
  export const getContentSigner = (): ContentSigner | undefined => {
89
89
  if (!usesBetterAuthJwt()) return undefined;
90
90
  const secret = process.env.BETTER_AUTH_SECRET;
91
91
  if (!secret || secret.length === 0) {
92
92
  throw new Error(
93
- "a self-host SQL backend (postgres/sqlite) requires BETTER_AUTH_SECRET to sign content URLs",
93
+ "the self-host SQL backend requires BETTER_AUTH_SECRET to sign content URLs",
94
94
  );
95
95
  }
96
96
  return createContentSigner(secret);
@@ -19,7 +19,7 @@ export interface BuildContentUrlInput {
19
19
  messageId: string;
20
20
  partPath: string;
21
21
  /**
22
- * When present (Postgres stack), append an `exp`/`sig` signature scoped to
22
+ * When present (self-host stack), append an `exp`/`sig` signature scoped to
23
23
  * this account's storage path. Absent on AWS, where Lambda@Edge guards the
24
24
  * URL and the path stays unsigned. See `contentSignature.ts`.
25
25
  */
@@ -16,7 +16,7 @@ import type { Context } from "openapi-backend";
16
16
  import { getAccountConfigIdFromEvent } from "../auth.js";
17
17
  import { getMsOAuthConfig } from "../config/msoauth.js";
18
18
  import { safeJsonParse } from "../json.js";
19
- import { getClient } from "../service/dynamodb.js";
19
+ import { getClient } from "../service/data-client.js";
20
20
  import type { MicrosoftOAuthOperationIds, OperationHandler } from "../types.js";
21
21
  import { triggerAccountSyncSafe } from "./account.js";
22
22
  import { findActiveDuplicateMailbox } from "./account-guards.js";
@@ -29,7 +29,7 @@ import type { APIGatewayProxyEvent } from "aws-lambda";
29
29
  import { env } from "expect-env";
30
30
  import type { Context } from "openapi-backend";
31
31
  import { getAccountConfigIdFromEvent } from "../auth.js";
32
- import { getClient } from "../service/dynamodb.js";
32
+ import { getClient } from "../service/data-client.js";
33
33
  import { fireAndForget } from "../service/fire-and-forget.js";
34
34
  import { sqsClient } from "../service/sqs.js";
35
35
  import { triggerAccountSync } from "../service/trigger-sync.js";
@@ -7,7 +7,7 @@ import { ForbiddenError } from "@remit/data-ports/errors";
7
7
  import type { APIGatewayProxyEvent } from "aws-lambda";
8
8
  import type { Context } from "openapi-backend";
9
9
  import { getAccountConfigIdFromEvent } from "../auth.js";
10
- import { getClient } from "../service/dynamodb.js";
10
+ import { getClient } from "../service/data-client.js";
11
11
  import type {
12
12
  AddressDetailOperationIds,
13
13
  AddressOperationIds,
@@ -10,7 +10,7 @@ import type { APIGatewayProxyEvent } from "aws-lambda";
10
10
  import { env } from "expect-env";
11
11
  import type { Context } from "openapi-backend";
12
12
  import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
13
- import { getClient } from "../service/dynamodb.js";
13
+ import { getClient } from "../service/data-client.js";
14
14
  import { fireAndForget } from "../service/fire-and-forget.js";
15
15
  import { sqsClient } from "../service/sqs.js";
16
16
  import { triggerAccountSync } from "../service/trigger-sync.js";
@@ -13,7 +13,7 @@ import { FilterScope, FilterState } from "@remit/domain-enums";
13
13
  import type { AnchorPayload } from "@remit/search-service";
14
14
  import type { APIGatewayProxyEvent } from "aws-lambda";
15
15
  import { getAccountConfigIdFromEvent } from "../auth.js";
16
- import { getClient } from "../service/dynamodb.js";
16
+ import { getClient } from "../service/data-client.js";
17
17
  import { buildFilterAnchor } from "../service/filter.js";
18
18
  import type {
19
19
  FilterDetailOperationIds,
@@ -4,7 +4,7 @@ import type {
4
4
  } from "@remit/api-openapi-types";
5
5
  import type { APIGatewayProxyEvent } from "aws-lambda";
6
6
  import { getAccountConfigIdFromEvent } from "../auth.js";
7
- import { getClient } from "../service/dynamodb.js";
7
+ import { getClient } from "../service/data-client.js";
8
8
  import type { FolderRoleOperationIds, OperationHandler } from "../types.js";
9
9
  import { toAccountResponse } from "./account-guards.js";
10
10
  import { loadAccountOverrides } from "./account-overrides.js";
@@ -11,7 +11,7 @@ import type {
11
11
  } from "@remit/data-ports";
12
12
  import type { APIGatewayProxyEvent } from "aws-lambda";
13
13
  import { getAccountConfigIdFromEvent } from "../auth.js";
14
- import { getClient } from "../service/dynamodb.js";
14
+ import { getClient } from "../service/data-client.js";
15
15
  import type {
16
16
  LabelDetailOperationIds,
17
17
  LabelOperationIds,
@@ -11,7 +11,7 @@ import {
11
11
  applyPendingMoveCountPrediction,
12
12
  type PendingUnseenFlagPush,
13
13
  } from "../derive/pendingMoveCounts.js";
14
- import { getClient } from "../service/dynamodb.js";
14
+ import { getClient } from "../service/data-client.js";
15
15
  import type {
16
16
  MailboxDetailOperationIds,
17
17
  MailboxOperationIds,
@@ -139,7 +139,7 @@ export const assertMailboxInAccount = (
139
139
  * `applyPendingMoveCountPrediction`'s read-time adjustment only, never mutates
140
140
  * stored counts (epic #1281 invariant 4). Markers are written by the
141
141
  * imap-worker bulk sync path through `RemitClient.placementMove` on every
142
- * backend, so this is a real signal on Postgres too.
142
+ * backend, so this is a real signal on the self-host stack too.
143
143
  */
144
144
  const loadPendingMoves = (
145
145
  client: Awaited<ReturnType<typeof getClient>>,
@@ -150,7 +150,7 @@ const loadPendingMoves = (
150
150
  * Every pending `\Seen` flag-push marker (issue #1273) for an account —
151
151
  * `\Flagged` (star) markers are excluded, since only read/unread state feeds
152
152
  * `unseenCount`'s prediction. `RemitClient.flagPush` is present and WRITTEN
153
- * on both backends, so this is a real signal on Postgres too.
153
+ * on both backends, so this is a real signal on the self-host stack too.
154
154
  */
155
155
  const loadPendingUnseenFlagPushes = async (
156
156
  client: Awaited<ReturnType<typeof getClient>>,
@@ -8,7 +8,7 @@ import {
8
8
  _resetForTest,
9
9
  type RemitClient,
10
10
  setClient,
11
- } from "../service/dynamodb.js";
11
+ } from "../service/data-client.js";
12
12
  import { MeOperations } from "./me.js";
13
13
 
14
14
  const listQuarantine = MeOperations.MeOperations_listQuarantine as unknown as (
@@ -13,7 +13,7 @@ import type { APIGatewayProxyEvent } from "aws-lambda";
13
13
  import { env } from "expect-env";
14
14
  import type { Context } from "openapi-backend";
15
15
  import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
16
- import { getClient } from "../service/dynamodb.js";
16
+ import { getClient } from "../service/data-client.js";
17
17
  import { sqsClient } from "../service/sqs.js";
18
18
  import type { MeOperationIds, OperationHandler } from "../types.js";
19
19
  import { toVipSuggestionEntry } from "./vip-suggestions.js";
@@ -41,7 +41,7 @@ import {
41
41
  getContentDeliveryDomain,
42
42
  } from "../derive/contentUrl.js";
43
43
  import { deriveSenderTrust } from "../derive/senderTrust.js";
44
- import { getClient } from "../service/dynamodb.js";
44
+ import { getClient } from "../service/data-client.js";
45
45
  import type {
46
46
  MessageBulkOperationIds,
47
47
  MessageOperationIds,
@@ -1,7 +1,9 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
  import type { OrganizeInput } from "@remit/api-openapi-types";
4
+ import { BadRequestError } from "@remit/data-ports/errors";
4
5
  import { FilterMatchOperator } from "@remit/domain-enums";
6
+ import { handleError } from "../error.js";
5
7
  import { predicateFromInput } from "./organize.js";
6
8
 
7
9
  const input = (over: Partial<OrganizeInput> = {}): OrganizeInput => ({
@@ -41,3 +43,21 @@ describe("predicateFromInput (move back-apply accepted)", () => {
41
43
  assert.equal(predicate.actionLabelId, "None");
42
44
  });
43
45
  });
46
+
47
+ // previewOrganize rejects a body-content (HasWords) clause by throwing
48
+ // BadRequestError (see service/organize.test.ts for the throw site). Proving
49
+ // the 4xx actually reaches the wire — not just that the right class is
50
+ // thrown — means proving the shared error handler maps it, since a plain
51
+ // Error would otherwise fall through to a 500 (reader #457).
52
+ describe("previewOrganize rejected-rule response (reader #457)", () => {
53
+ it("maps a rejected HasWords clause to a 400 naming the reason, not a 500", async () => {
54
+ const error = new BadRequestError(
55
+ "Organize literal matching cannot evaluate a body-content (HasWords) clause without the vector pipeline — it requires the semantic widen path",
56
+ );
57
+
58
+ const response = await handleError(error);
59
+
60
+ assert.equal(response.statusCode, 400);
61
+ assert.match(JSON.parse(response.body).message, /HasWords/);
62
+ });
63
+ });
@@ -11,7 +11,7 @@ import type { APIGatewayProxyEvent } from "aws-lambda";
11
11
  import { env } from "expect-env";
12
12
  import type { Context } from "openapi-backend";
13
13
  import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
14
- import { getClient, type RemitClient } from "../service/dynamodb.js";
14
+ import { getClient, type RemitClient } from "../service/data-client.js";
15
15
  import {
16
16
  buildOrganizeMatchDeps,
17
17
  matchOrganize,
@@ -8,7 +8,7 @@ import { ForbiddenError } from "@remit/data-ports/errors";
8
8
  import type { APIGatewayProxyEvent } from "aws-lambda";
9
9
  import type { Context } from "openapi-backend";
10
10
  import { getAccountConfigIdFromEvent } from "../auth.js";
11
- import { getClient } from "../service/dynamodb.js";
11
+ import { getClient } from "../service/data-client.js";
12
12
  import type {
13
13
  OperationHandler,
14
14
  OutboxDetailOperationIds,
@@ -6,7 +6,7 @@ import type { SearchResult } from "@remit/search-service";
6
6
  import type { APIGatewayProxyEvent } from "aws-lambda";
7
7
  import type { Context } from "openapi-backend";
8
8
  import { getAccountConfigIdFromEvent } from "../auth.js";
9
- import { getClient } from "../service/dynamodb.js";
9
+ import { getClient } from "../service/data-client.js";
10
10
  import {
11
11
  isSemanticSearchUnavailable,
12
12
  noteSemanticCapabilityAbsence,
@@ -3,7 +3,7 @@ import { logger } from "@remit/logger-lambda";
3
3
  import type { APIGatewayProxyEvent } from "aws-lambda";
4
4
  import { env } from "expect-env";
5
5
  import { getAccountConfigIdFromEvent } from "../auth.js";
6
- import { getClient } from "../service/dynamodb.js";
6
+ import { getClient } from "../service/data-client.js";
7
7
  import {
8
8
  type FireAndForgetLogger,
9
9
  fireAndForget,
@@ -17,7 +17,7 @@ import {
17
17
  filterByOffRowCriteria,
18
18
  hasOffRowCriteria,
19
19
  } from "../derive/filterThreadCriteria.js";
20
- import { getClient } from "../service/dynamodb.js";
20
+ import { getClient } from "../service/data-client.js";
21
21
  import type {
22
22
  OperationHandler,
23
23
  ThreadDetailOperationIds,
@@ -9,7 +9,7 @@ import type { Context } from "openapi-backend";
9
9
  import pMap from "p-map";
10
10
  import { getAccountConfigIdFromEvent } from "../auth.js";
11
11
  import { enrichThreadRows } from "../derive/enrichThreadRows.js";
12
- import { getClient } from "../service/dynamodb.js";
12
+ import { getClient } from "../service/data-client.js";
13
13
  import type { OperationHandler, UnifiedThreadOperationIds } from "../types.js";
14
14
  import {
15
15
  groupAccountOverrides,
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ import { assertLocalBypassNotInDeployedEnv } from "./auth.js";
14
14
  import { usesBetterAuthJwt } from "./data-backend.js";
15
15
  import { handleError } from "./error.js";
16
16
  import { handlers } from "./handlers/index.js";
17
- import { authenticatePostgresRequest } from "./jwt-auth.js";
17
+ import { authenticateSelfHostRequest } from "./jwt-auth.js";
18
18
  import { normalizeRequest } from "./request.js";
19
19
  import { runWithRequestContext } from "./request-context.js";
20
20
  import { formatResponse, postResponseHandler } from "./response.js";
@@ -161,7 +161,7 @@ const rawHandler = async (event: APIGatewayProxyEvent, context: Context) =>
161
161
  const origin = readOriginHeader(event.headers);
162
162
 
163
163
  if (usesBetterAuthJwt()) {
164
- const denied = await authenticatePostgresRequest(event);
164
+ const denied = await authenticateSelfHostRequest(event);
165
165
  if (denied) return denied;
166
166
  }
167
167