@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.
|
|
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
|
+
};
|