@remit/backend 0.0.18 → 0.0.19

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -45,8 +45,10 @@
45
45
  "@aws-sdk/core": "*",
46
46
  "@remit/electrodb-entities": "*",
47
47
  "@remit/search-index-worker": "*",
48
+ "@types/better-sqlite3": "^7.6.13",
48
49
  "@types/express": "^5.0.3",
49
50
  "@types/swagger-ui-express": "^4.1.8",
51
+ "better-sqlite3": "^12.11.1",
50
52
  "electrodb": "*",
51
53
  "esbuild": "^0.27.7",
52
54
  "express": "^5.1.0",
@@ -0,0 +1,89 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { after, afterEach, before, describe, it } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createInstanceOwnerStore } from "@remit/auth-service";
7
+ import type { APIGatewayProxyEvent } from "aws-lambda";
8
+ import Database from "better-sqlite3";
9
+ import { requireInstanceOwner } from "./owner-guard.js";
10
+
11
+ const tmpRoot = join(
12
+ fileURLToPath(new URL(".", import.meta.url)),
13
+ "..",
14
+ "..",
15
+ ".tmp",
16
+ "owner-guard",
17
+ );
18
+
19
+ const readMigrationSql = (relativePath: string): string =>
20
+ readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
21
+
22
+ const authMigrationSql = readMigrationSql(
23
+ "../../../../deploy/vps/migrations-sqlite/auth/0000_rainy_iron_monger.sql",
24
+ );
25
+ const metaMigrationSql = readMigrationSql(
26
+ "../../../../deploy/vps/migrations-sqlite/meta/0000_demonic_lilith.sql",
27
+ );
28
+
29
+ let dbPath: string;
30
+
31
+ before(() => {
32
+ mkdirSync(tmpRoot, { recursive: true });
33
+ dbPath = join(mkdtempSync(join(tmpRoot, "db-")), "app.db");
34
+ const sqlite = new Database(dbPath);
35
+ for (const sql of [authMigrationSql, metaMigrationSql]) {
36
+ for (const statement of sql.split("--> statement-breakpoint")) {
37
+ sqlite.exec(statement);
38
+ }
39
+ }
40
+ sqlite
41
+ .prepare(
42
+ "INSERT INTO auth_user (id, name, email, email_verified, created_at, updated_at) VALUES (?, ?, ?, 0, ?, ?)",
43
+ )
44
+ .run("the-owner", "the-owner", "the-owner@example.com", 1000, 1000);
45
+ sqlite.close();
46
+ process.env.DATA_BACKEND = "sqlite";
47
+ process.env.SQLITE_DB_PATH = dbPath;
48
+ });
49
+
50
+ after(() => {
51
+ rmSync(tmpRoot, { recursive: true, force: true });
52
+ });
53
+
54
+ afterEach(() => {
55
+ delete process.env.REMIT_OWNER_EMAIL;
56
+ });
57
+
58
+ const buildEvent = (sub?: string): APIGatewayProxyEvent =>
59
+ ({
60
+ headers: {},
61
+ requestContext: sub ? { authorizer: { claims: { sub } } } : {},
62
+ }) as unknown as APIGatewayProxyEvent;
63
+
64
+ describe("requireInstanceOwner", () => {
65
+ it("rejects a request with no authenticated caller", async () => {
66
+ const result = await requireInstanceOwner(buildEvent());
67
+
68
+ assert.equal(result?.statusCode, 403);
69
+ });
70
+
71
+ it("rejects a caller who has not claimed ownership", async () => {
72
+ const result = await requireInstanceOwner(buildEvent("not-the-owner"));
73
+
74
+ assert.equal(result?.statusCode, 403);
75
+ });
76
+
77
+ it("lets the instance owner through", async () => {
78
+ const store = await createInstanceOwnerStore({
79
+ provider: "sqlite",
80
+ connectionString: dbPath,
81
+ });
82
+ await store.claimIfUnclaimed("the-owner");
83
+ await store.close();
84
+
85
+ const result = await requireInstanceOwner(buildEvent("the-owner"));
86
+
87
+ assert.equal(result, null);
88
+ });
89
+ });
@@ -0,0 +1,25 @@
1
+ import { isInstanceOwner } from "@remit/auth-service";
2
+ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
3
+ import { getSubFromEvent } from "../auth.js";
4
+
5
+ const forbidden = (message: string): APIGatewayProxyResult => ({
6
+ statusCode: 403,
7
+ headers: { "Content-Type": "application/json" },
8
+ body: JSON.stringify({ message }),
9
+ });
10
+
11
+ /**
12
+ * Gate a request to the standalone instance owner (RFC 037 D8) — the first
13
+ * account to register, or whoever `REMIT_OWNER_EMAIL` names. Returns `null` to
14
+ * let the request proceed, or a 403 `APIGatewayProxyResult` otherwise. Callers
15
+ * wire this in ahead of the handler; it does not run the handler itself.
16
+ */
17
+ export const requireInstanceOwner = async (
18
+ event: APIGatewayProxyEvent,
19
+ ): Promise<APIGatewayProxyResult | null> => {
20
+ const sub = getSubFromEvent(event);
21
+ if (!sub) return forbidden("Instance owner required");
22
+ if (!(await isInstanceOwner(sub)))
23
+ return forbidden("Instance owner required");
24
+ return null;
25
+ };