@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.
- package/dev-server/content-auth.test.ts +9 -9
- package/dev-server/content-auth.ts +2 -2
- package/dev-server/relational-health.test.ts +0 -8
- package/dev-server/relational-health.ts +3 -21
- package/dev-server/server.ts +7 -9
- package/package.json +4 -7
- package/scripts/backfill-list-id.ts +1 -1
- package/src/data-backend.test.ts +1 -3
- package/src/data-backend.ts +8 -14
- package/src/derive/contentSignature.test.ts +1 -19
- package/src/derive/contentSignature.ts +10 -10
- package/src/derive/contentUrl.ts +1 -1
- package/src/handlers/account-oauth.ts +1 -1
- package/src/handlers/account.ts +1 -1
- package/src/handlers/address.ts +1 -1
- package/src/handlers/config.ts +1 -1
- package/src/handlers/filter.ts +1 -1
- package/src/handlers/folder-role.ts +1 -1
- package/src/handlers/label.ts +1 -1
- package/src/handlers/mailbox.ts +3 -3
- package/src/handlers/me.test.ts +1 -1
- package/src/handlers/me.ts +1 -1
- package/src/handlers/message.ts +1 -1
- package/src/handlers/organize.test.ts +20 -0
- package/src/handlers/organize.ts +1 -1
- package/src/handlers/outbox.ts +1 -1
- package/src/handlers/search.ts +1 -1
- package/src/handlers/sync.ts +1 -1
- package/src/handlers/thread.ts +1 -1
- package/src/handlers/unified-threads.ts +1 -1
- package/src/index.ts +2 -2
- package/src/jwt-auth.test.ts +6 -6
- package/src/jwt-auth.ts +2 -2
- package/src/service/compose-sqlite.ts +8 -14
- package/src/service/create-remit-client.ts +6 -6
- package/src/service/data-client.test.ts +96 -0
- package/src/service/data-client.ts +58 -0
- package/src/service/organize.test.ts +9 -4
- package/src/service/organize.ts +5 -7
- package/src/service/semantic-capability.test.ts +10 -16
- package/src/service/semantic-capability.ts +1 -1
- package/src/service/compose-postgres.ts +0 -76
- package/src/service/dynamodb.test.ts +0 -98
- 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
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
77
|
+
it("fails closed with 500 when no signing secret is configured", () => {
|
|
78
78
|
const result = authorizeContentRequest({
|
|
79
|
-
dataBackend: "
|
|
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
|
|
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
|
|
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 !== "
|
|
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
|
|
40
|
-
*
|
|
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
|
-
|
|
49
|
-
await pingSqlite();
|
|
50
|
-
} else {
|
|
51
|
-
await pingPostgres();
|
|
52
|
-
}
|
|
34
|
+
await pingSqlite();
|
|
53
35
|
return true;
|
|
54
36
|
} catch {
|
|
55
37
|
return false;
|
package/dev-server/server.ts
CHANGED
|
@@ -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/
|
|
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
|
|
32
|
-
//
|
|
33
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
16
|
-
"default": "./src/service/
|
|
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/
|
|
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
|
package/src/data-backend.test.ts
CHANGED
|
@@ -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
|
|
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
|
});
|
package/src/data-backend.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
-
"
|
|
93
|
+
"the self-host SQL backend requires BETTER_AUTH_SECRET to sign content URLs",
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
return createContentSigner(secret);
|
package/src/derive/contentUrl.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface BuildContentUrlInput {
|
|
|
19
19
|
messageId: string;
|
|
20
20
|
partPath: string;
|
|
21
21
|
/**
|
|
22
|
-
* When present (
|
|
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/
|
|
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";
|
package/src/handlers/account.ts
CHANGED
|
@@ -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/
|
|
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";
|
package/src/handlers/address.ts
CHANGED
|
@@ -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/
|
|
10
|
+
import { getClient } from "../service/data-client.js";
|
|
11
11
|
import type {
|
|
12
12
|
AddressDetailOperationIds,
|
|
13
13
|
AddressOperationIds,
|
package/src/handlers/config.ts
CHANGED
|
@@ -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/
|
|
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";
|
package/src/handlers/filter.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
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";
|
package/src/handlers/label.ts
CHANGED
|
@@ -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/
|
|
14
|
+
import { getClient } from "../service/data-client.js";
|
|
15
15
|
import type {
|
|
16
16
|
LabelDetailOperationIds,
|
|
17
17
|
LabelOperationIds,
|
package/src/handlers/mailbox.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
applyPendingMoveCountPrediction,
|
|
12
12
|
type PendingUnseenFlagPush,
|
|
13
13
|
} from "../derive/pendingMoveCounts.js";
|
|
14
|
-
import { getClient } from "../service/
|
|
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
|
|
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
|
|
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>>,
|
package/src/handlers/me.test.ts
CHANGED
package/src/handlers/me.ts
CHANGED
|
@@ -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/
|
|
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";
|
package/src/handlers/message.ts
CHANGED
|
@@ -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/
|
|
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
|
+
});
|
package/src/handlers/organize.ts
CHANGED
|
@@ -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/
|
|
14
|
+
import { getClient, type RemitClient } from "../service/data-client.js";
|
|
15
15
|
import {
|
|
16
16
|
buildOrganizeMatchDeps,
|
|
17
17
|
matchOrganize,
|
package/src/handlers/outbox.ts
CHANGED
|
@@ -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/
|
|
11
|
+
import { getClient } from "../service/data-client.js";
|
|
12
12
|
import type {
|
|
13
13
|
OperationHandler,
|
|
14
14
|
OutboxDetailOperationIds,
|
package/src/handlers/search.ts
CHANGED
|
@@ -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/
|
|
9
|
+
import { getClient } from "../service/data-client.js";
|
|
10
10
|
import {
|
|
11
11
|
isSemanticSearchUnavailable,
|
|
12
12
|
noteSemanticCapabilityAbsence,
|
package/src/handlers/sync.ts
CHANGED
|
@@ -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/
|
|
6
|
+
import { getClient } from "../service/data-client.js";
|
|
7
7
|
import {
|
|
8
8
|
type FireAndForgetLogger,
|
|
9
9
|
fireAndForget,
|
package/src/handlers/thread.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
filterByOffRowCriteria,
|
|
18
18
|
hasOffRowCriteria,
|
|
19
19
|
} from "../derive/filterThreadCriteria.js";
|
|
20
|
-
import { getClient } from "../service/
|
|
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/
|
|
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 {
|
|
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
|
|
164
|
+
const denied = await authenticateSelfHostRequest(event);
|
|
165
165
|
if (denied) return denied;
|
|
166
166
|
}
|
|
167
167
|
|