@remit/backend 0.0.17 → 0.0.18
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.
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmodSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { checkRelationalStore } from "./relational-health.js";
|
|
8
|
+
|
|
9
|
+
describe("checkRelationalStore", () => {
|
|
10
|
+
const ORIGINAL_DATA_BACKEND = process.env.DATA_BACKEND;
|
|
11
|
+
const ORIGINAL_SQLITE_DB_PATH = process.env.SQLITE_DB_PATH;
|
|
12
|
+
let dir: string | undefined;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (ORIGINAL_DATA_BACKEND === undefined) delete process.env.DATA_BACKEND;
|
|
16
|
+
else process.env.DATA_BACKEND = ORIGINAL_DATA_BACKEND;
|
|
17
|
+
if (ORIGINAL_SQLITE_DB_PATH === undefined) {
|
|
18
|
+
delete process.env.SQLITE_DB_PATH;
|
|
19
|
+
} else {
|
|
20
|
+
process.env.SQLITE_DB_PATH = ORIGINAL_SQLITE_DB_PATH;
|
|
21
|
+
}
|
|
22
|
+
if (dir) {
|
|
23
|
+
rmSync(dir, { recursive: true, force: true });
|
|
24
|
+
dir = undefined;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("is true without touching disk on the AWS/DynamoDB path", async () => {
|
|
29
|
+
delete process.env.DATA_BACKEND;
|
|
30
|
+
delete process.env.SQLITE_DB_PATH;
|
|
31
|
+
assert.equal(await checkRelationalStore(), true);
|
|
32
|
+
|
|
33
|
+
process.env.DATA_BACKEND = "dynamodb";
|
|
34
|
+
assert.equal(await checkRelationalStore(), true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("is true when the SQLite file is reachable", async () => {
|
|
38
|
+
dir = mkdtempSync(join(tmpdir(), "remit-health-"));
|
|
39
|
+
const dbPath = join(dir, "remit.db");
|
|
40
|
+
new Database(dbPath).close();
|
|
41
|
+
|
|
42
|
+
process.env.DATA_BACKEND = "sqlite";
|
|
43
|
+
process.env.SQLITE_DB_PATH = dbPath;
|
|
44
|
+
|
|
45
|
+
assert.equal(await checkRelationalStore(), true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("is false when SQLITE_DB_PATH points at a directory that does not exist", async () => {
|
|
49
|
+
process.env.DATA_BACKEND = "sqlite";
|
|
50
|
+
process.env.SQLITE_DB_PATH = "/nonexistent/remit.db";
|
|
51
|
+
|
|
52
|
+
assert.equal(await checkRelationalStore(), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("is false when the SQLite file exists but is unreadable", async () => {
|
|
56
|
+
dir = mkdtempSync(join(tmpdir(), "remit-health-"));
|
|
57
|
+
const dbPath = join(dir, "remit.db");
|
|
58
|
+
new Database(dbPath).close();
|
|
59
|
+
chmodSync(dbPath, 0o000);
|
|
60
|
+
|
|
61
|
+
process.env.DATA_BACKEND = "sqlite";
|
|
62
|
+
process.env.SQLITE_DB_PATH = dbPath;
|
|
63
|
+
|
|
64
|
+
assert.equal(await checkRelationalStore(), false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("is false when the file is readable but no longer writable", async () => {
|
|
68
|
+
dir = mkdtempSync(join(tmpdir(), "remit-health-"));
|
|
69
|
+
const dbPath = join(dir, "remit.db");
|
|
70
|
+
new Database(dbPath).close();
|
|
71
|
+
|
|
72
|
+
process.env.DATA_BACKEND = "sqlite";
|
|
73
|
+
process.env.SQLITE_DB_PATH = dbPath;
|
|
74
|
+
assert.equal(await checkRelationalStore(), true);
|
|
75
|
+
|
|
76
|
+
// Readable, not writable: the read-only SELECT ping still opens and
|
|
77
|
+
// succeeds, so without `access(W_OK)` this reports healthy on a store the
|
|
78
|
+
// backend can no longer write to. This is the case the writability check
|
|
79
|
+
// exists to catch — dropping `access()` from pingSqlite fails this test.
|
|
80
|
+
chmodSync(dbPath, 0o400);
|
|
81
|
+
assert.equal(await checkRelationalStore(), false);
|
|
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
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { access, constants } from "node:fs/promises";
|
|
2
|
+
import { isSelfHostSqlBackend } from "../src/data-backend.js";
|
|
3
|
+
|
|
4
|
+
// The SELECT ping opens a read-only connection, so it keeps succeeding on a
|
|
5
|
+
// file that can still be read but no longer written — permissions revoked to
|
|
6
|
+
// read-only, or the volume remounted read-only under the process. The backend
|
|
7
|
+
// must write (better-auth persists sessions to this same file, RFC 036 D3), so
|
|
8
|
+
// a readable-but-unwritable store has to report unhealthy. `access(R_OK|W_OK)`
|
|
9
|
+
// checks both bits in one fresh syscall; the read-only query never covers
|
|
10
|
+
// writability. The query after it still exercises the SQL engine itself.
|
|
11
|
+
const pingSqlite = async (): Promise<void> => {
|
|
12
|
+
const path = process.env.SQLITE_DB_PATH ?? "";
|
|
13
|
+
await access(path, constants.R_OK | constants.W_OK);
|
|
14
|
+
|
|
15
|
+
const { default: Database } = await import("better-sqlite3");
|
|
16
|
+
const db = new Database(path, { readonly: true });
|
|
17
|
+
try {
|
|
18
|
+
db.prepare("SELECT 1").get();
|
|
19
|
+
} finally {
|
|
20
|
+
db.close();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
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
|
+
/**
|
|
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
|
|
41
|
+
* booted with an unusable database reports unhealthy instead of a bare 200. The
|
|
42
|
+
* AWS/DynamoDB path has no relational store and is never checked.
|
|
43
|
+
*/
|
|
44
|
+
export const checkRelationalStore = async (): Promise<boolean> => {
|
|
45
|
+
if (!isSelfHostSqlBackend()) return true;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
if (process.env.DATA_BACKEND === "sqlite") {
|
|
49
|
+
await pingSqlite();
|
|
50
|
+
} else {
|
|
51
|
+
await pingPostgres();
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
};
|
package/dev-server/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { serveContent } from "./content-handler.js";
|
|
|
16
16
|
import { resolveContentPath } from "./content-path.js";
|
|
17
17
|
import { parseAllowedOrigins, resolveAllowOrigin } from "./cors.js";
|
|
18
18
|
import { createLambdaContext, createLambdaEvent } from "./lambda-helpers.js";
|
|
19
|
+
import { checkRelationalStore } from "./relational-health.js";
|
|
19
20
|
|
|
20
21
|
const app = express();
|
|
21
22
|
|
|
@@ -123,7 +124,12 @@ app.use((req: Request, res: Response, next: NextFunction) => {
|
|
|
123
124
|
|
|
124
125
|
app.get("/.well-known/appspecific/com.chrome.devtools.json", () => ({}));
|
|
125
126
|
|
|
126
|
-
app.get("/health", (_req: Request, res: Response) => {
|
|
127
|
+
app.get("/health", async (_req: Request, res: Response) => {
|
|
128
|
+
const relationalStoreReachable = await checkRelationalStore();
|
|
129
|
+
if (!relationalStoreReachable) {
|
|
130
|
+
res.status(503).json({ status: "unhealthy" });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
127
133
|
res.json({
|
|
128
134
|
status: "ok",
|
|
129
135
|
timestamp: new Date().toISOString(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/backend",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Remit Mail Inspector API backend",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "",
|
|
@@ -70,11 +70,15 @@
|
|
|
70
70
|
"@remit/sqs-client": "*",
|
|
71
71
|
"@aws-sdk/client-secrets-manager": "*",
|
|
72
72
|
"@aws-sdk/client-sqs": "*",
|
|
73
|
+
"better-sqlite3": "^12.11.1",
|
|
73
74
|
"drizzle-orm": "^0.45.2",
|
|
74
75
|
"expect-env": "*",
|
|
75
76
|
"openapi-backend": "*",
|
|
76
77
|
"p-map": "*",
|
|
77
|
-
"
|
|
78
|
+
"pg": "^8.0.0",
|
|
79
|
+
"@types/aws-lambda": "*",
|
|
80
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
81
|
+
"@types/pg": "^8.0.0"
|
|
78
82
|
},
|
|
79
83
|
"publishConfig": {
|
|
80
84
|
"access": "public"
|