@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
+ };
@@ -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.17",
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
- "@types/aws-lambda": "*"
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"