@remit/backend 0.0.30 → 0.0.32
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
|
@@ -9,9 +9,7 @@ import {
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { after, afterEach, before, beforeEach, describe, it } from "node:test";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { createInstanceOwnerStore } from "@remit/auth-service";
|
|
13
12
|
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
14
|
-
import Database from "better-sqlite3";
|
|
15
13
|
import type { Context } from "openapi-backend";
|
|
16
14
|
import { SystemOperations } from "./system-update.js";
|
|
17
15
|
|
|
@@ -23,20 +21,9 @@ const tmpRoot = join(
|
|
|
23
21
|
"system-update",
|
|
24
22
|
);
|
|
25
23
|
|
|
26
|
-
const
|
|
27
|
-
readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
|
|
28
|
-
|
|
29
|
-
const authMigrationSql = readMigrationSql(
|
|
30
|
-
"../../../../deploy/vps/migrations-sqlite/auth/0000_rainy_iron_monger.sql",
|
|
31
|
-
);
|
|
32
|
-
const metaMigrationSql = readMigrationSql(
|
|
33
|
-
"../../../../deploy/vps/migrations-sqlite/meta/0000_demonic_lilith.sql",
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const OWNER = "the-owner";
|
|
24
|
+
const USER = "any-signed-in-user";
|
|
37
25
|
const MANIFEST_URL = "https://updates.example.com/stable.json";
|
|
38
26
|
|
|
39
|
-
let dbPath: string;
|
|
40
27
|
let controlDir: string;
|
|
41
28
|
|
|
42
29
|
const okState = {
|
|
@@ -57,29 +44,8 @@ const writeState = (state: unknown): void => {
|
|
|
57
44
|
writeFileSync(join(controlDir, "state.json"), JSON.stringify(state));
|
|
58
45
|
};
|
|
59
46
|
|
|
60
|
-
before(
|
|
47
|
+
before(() => {
|
|
61
48
|
mkdirSync(tmpRoot, { recursive: true });
|
|
62
|
-
dbPath = join(mkdtempSync(join(tmpRoot, "db-")), "app.db");
|
|
63
|
-
const sqlite = new Database(dbPath);
|
|
64
|
-
for (const sql of [authMigrationSql, metaMigrationSql]) {
|
|
65
|
-
for (const statement of sql.split("--> statement-breakpoint")) {
|
|
66
|
-
sqlite.exec(statement);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
sqlite
|
|
70
|
-
.prepare(
|
|
71
|
-
"INSERT INTO auth_user (id, name, email, email_verified, created_at, updated_at) VALUES (?, ?, ?, 0, ?, ?)",
|
|
72
|
-
)
|
|
73
|
-
.run(OWNER, OWNER, "owner@example.com", 1000, 1000);
|
|
74
|
-
sqlite.close();
|
|
75
|
-
process.env.DATA_BACKEND = "sqlite";
|
|
76
|
-
process.env.SQLITE_DB_PATH = dbPath;
|
|
77
|
-
const store = await createInstanceOwnerStore({
|
|
78
|
-
provider: "sqlite",
|
|
79
|
-
connectionString: dbPath,
|
|
80
|
-
});
|
|
81
|
-
await store.claimIfUnclaimed(OWNER);
|
|
82
|
-
await store.close();
|
|
83
49
|
});
|
|
84
50
|
|
|
85
51
|
after(() => {
|
|
@@ -135,30 +101,43 @@ describe("GET /system/update", () => {
|
|
|
135
101
|
it("returns 404 when no manifest URL is configured", async () => {
|
|
136
102
|
delete process.env.REMIT_UPDATE_MANIFEST_URL;
|
|
137
103
|
|
|
138
|
-
const result = await getUpdate(buildEvent(
|
|
104
|
+
const result = await getUpdate(buildEvent(USER));
|
|
139
105
|
|
|
140
106
|
assert.ok(hasStatus(result, 404));
|
|
141
107
|
});
|
|
142
108
|
|
|
143
|
-
it("returns
|
|
144
|
-
|
|
109
|
+
it("returns 401 when the caller is not authenticated", async () => {
|
|
110
|
+
writeState(okState);
|
|
145
111
|
|
|
146
|
-
|
|
112
|
+
const result = await getUpdate(buildEvent());
|
|
113
|
+
|
|
114
|
+
assert.ok(hasStatus(result, 401));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns 200 for any authenticated caller", async () => {
|
|
118
|
+
writeState(okState);
|
|
119
|
+
|
|
120
|
+
const result = await getUpdate(buildEvent("another-signed-in-user"));
|
|
121
|
+
|
|
122
|
+
assert.deepEqual(result, okState);
|
|
147
123
|
});
|
|
148
124
|
|
|
149
125
|
it("returns 200 with the state file verbatim", async () => {
|
|
150
126
|
writeState(okState);
|
|
151
127
|
|
|
152
|
-
const result = await getUpdate(buildEvent(
|
|
128
|
+
const result = await getUpdate(buildEvent(USER));
|
|
153
129
|
|
|
154
130
|
assert.deepEqual(result, okState);
|
|
155
131
|
});
|
|
156
132
|
|
|
157
|
-
it("
|
|
158
|
-
|
|
133
|
+
it("reports an unknown version, not its own process env, when no state file exists", async () => {
|
|
134
|
+
// The updater owns the running version and writes it into state.json; with
|
|
135
|
+
// no state file the backend has nothing authoritative, so it says unknown
|
|
136
|
+
// rather than surfacing REMIT_TAG, which drifts from the real running tag.
|
|
137
|
+
const result = await getUpdate(buildEvent(USER));
|
|
159
138
|
|
|
160
139
|
assert.deepEqual(result, {
|
|
161
|
-
currentVersion: "
|
|
140
|
+
currentVersion: "unknown",
|
|
162
141
|
check: { status: "disabled" },
|
|
163
142
|
run: null,
|
|
164
143
|
});
|
|
@@ -186,7 +165,7 @@ describe("GET /system/update", () => {
|
|
|
186
165
|
run,
|
|
187
166
|
});
|
|
188
167
|
|
|
189
|
-
const result = (await getUpdate(buildEvent(
|
|
168
|
+
const result = (await getUpdate(buildEvent(USER))) as {
|
|
190
169
|
check: { status: string };
|
|
191
170
|
run: unknown;
|
|
192
171
|
};
|
|
@@ -203,7 +182,7 @@ describe("GET /system/update", () => {
|
|
|
203
182
|
run: null,
|
|
204
183
|
});
|
|
205
184
|
|
|
206
|
-
const result = (await getUpdate(buildEvent(
|
|
185
|
+
const result = (await getUpdate(buildEvent(USER))) as {
|
|
207
186
|
currentSchemaVersion?: number;
|
|
208
187
|
check: { schemaVersion?: number };
|
|
209
188
|
};
|
|
@@ -215,7 +194,7 @@ describe("GET /system/update", () => {
|
|
|
215
194
|
it("omits both schema-version fields when the state file predates them", async () => {
|
|
216
195
|
writeState(okState);
|
|
217
196
|
|
|
218
|
-
const result = (await getUpdate(buildEvent(
|
|
197
|
+
const result = (await getUpdate(buildEvent(USER))) as {
|
|
219
198
|
currentSchemaVersion?: number;
|
|
220
199
|
check: { schemaVersion?: number };
|
|
221
200
|
};
|
|
@@ -232,7 +211,7 @@ describe("GET /system/update", () => {
|
|
|
232
211
|
run: null,
|
|
233
212
|
});
|
|
234
213
|
|
|
235
|
-
const result = (await getUpdate(buildEvent(
|
|
214
|
+
const result = (await getUpdate(buildEvent(USER))) as {
|
|
236
215
|
currentSchemaVersion?: number;
|
|
237
216
|
check: { schemaVersion?: number };
|
|
238
217
|
};
|
|
@@ -248,7 +227,7 @@ describe("GET /system/update", () => {
|
|
|
248
227
|
run: null,
|
|
249
228
|
});
|
|
250
229
|
|
|
251
|
-
const result = (await getUpdate(buildEvent(
|
|
230
|
+
const result = (await getUpdate(buildEvent(USER))) as {
|
|
252
231
|
currentSchemaVersion?: number;
|
|
253
232
|
check: { schemaVersion?: number };
|
|
254
233
|
};
|
|
@@ -262,17 +241,34 @@ describe("POST /system/update", () => {
|
|
|
262
241
|
it("returns 404 when no manifest URL is configured", async () => {
|
|
263
242
|
delete process.env.REMIT_UPDATE_MANIFEST_URL;
|
|
264
243
|
|
|
265
|
-
const result = await applyUpdate("v1.5.0", buildEvent(
|
|
244
|
+
const result = await applyUpdate("v1.5.0", buildEvent(USER));
|
|
266
245
|
|
|
267
246
|
assert.ok(hasStatus(result, 404));
|
|
268
247
|
});
|
|
269
248
|
|
|
270
|
-
it("returns
|
|
249
|
+
it("returns 401 without writing request.json when the caller is not authenticated", async () => {
|
|
250
|
+
writeState(okState);
|
|
251
|
+
|
|
252
|
+
const result = await applyUpdate("v1.5.0", buildEvent());
|
|
253
|
+
|
|
254
|
+
assert.ok(hasStatus(result, 401));
|
|
255
|
+
assert.throws(() => readFileSync(join(controlDir, "request.json"), "utf8"));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("returns 202 for any authenticated caller and records that identity", async () => {
|
|
271
259
|
writeState(okState);
|
|
272
260
|
|
|
273
|
-
const result = await applyUpdate(
|
|
261
|
+
const result = (await applyUpdate(
|
|
262
|
+
"v1.5.0",
|
|
263
|
+
buildEvent("another-signed-in-user"),
|
|
264
|
+
)) as { statusCode: number };
|
|
265
|
+
|
|
266
|
+
assert.equal(result.statusCode, 202);
|
|
274
267
|
|
|
275
|
-
|
|
268
|
+
const request = JSON.parse(
|
|
269
|
+
readFileSync(join(controlDir, "request.json"), "utf8"),
|
|
270
|
+
);
|
|
271
|
+
assert.equal(request.requestedBy, "another-signed-in-user");
|
|
276
272
|
});
|
|
277
273
|
|
|
278
274
|
it("returns 409 when a run is already in flight", async () => {
|
|
@@ -291,7 +287,7 @@ describe("POST /system/update", () => {
|
|
|
291
287
|
},
|
|
292
288
|
});
|
|
293
289
|
|
|
294
|
-
await assert.rejects(applyUpdate("v1.5.0", buildEvent(
|
|
290
|
+
await assert.rejects(applyUpdate("v1.5.0", buildEvent(USER)), {
|
|
295
291
|
statusCode: 409,
|
|
296
292
|
});
|
|
297
293
|
});
|
|
@@ -299,13 +295,13 @@ describe("POST /system/update", () => {
|
|
|
299
295
|
it("returns 422 when the target is not the checked version", async () => {
|
|
300
296
|
writeState(okState);
|
|
301
297
|
|
|
302
|
-
await assert.rejects(applyUpdate("v9.9.9", buildEvent(
|
|
298
|
+
await assert.rejects(applyUpdate("v9.9.9", buildEvent(USER)), {
|
|
303
299
|
statusCode: 422,
|
|
304
300
|
});
|
|
305
301
|
});
|
|
306
302
|
|
|
307
303
|
it("returns 422 when no check has ever succeeded", async () => {
|
|
308
|
-
await assert.rejects(applyUpdate("v1.5.0", buildEvent(
|
|
304
|
+
await assert.rejects(applyUpdate("v1.5.0", buildEvent(USER)), {
|
|
309
305
|
statusCode: 422,
|
|
310
306
|
});
|
|
311
307
|
});
|
|
@@ -313,7 +309,7 @@ describe("POST /system/update", () => {
|
|
|
313
309
|
it("returns 202 with a run block carrying a fresh runId", async () => {
|
|
314
310
|
writeState(okState);
|
|
315
311
|
|
|
316
|
-
const result = (await applyUpdate("v1.5.0", buildEvent(
|
|
312
|
+
const result = (await applyUpdate("v1.5.0", buildEvent(USER))) as {
|
|
317
313
|
statusCode: number;
|
|
318
314
|
body: { run: { runId: string; targetVersion: string; outcome: null } };
|
|
319
315
|
};
|
|
@@ -327,7 +323,7 @@ describe("POST /system/update", () => {
|
|
|
327
323
|
it("writes request.json with exactly the four seam fields and nothing else", async () => {
|
|
328
324
|
writeState(okState);
|
|
329
325
|
|
|
330
|
-
await applyUpdate("v1.5.0", buildEvent(
|
|
326
|
+
await applyUpdate("v1.5.0", buildEvent(USER));
|
|
331
327
|
|
|
332
328
|
const request = JSON.parse(
|
|
333
329
|
readFileSync(join(controlDir, "request.json"), "utf8"),
|
|
@@ -340,7 +336,7 @@ describe("POST /system/update", () => {
|
|
|
340
336
|
"targetVersion",
|
|
341
337
|
]);
|
|
342
338
|
assert.equal(request.targetVersion, "v1.5.0");
|
|
343
|
-
assert.equal(request.requestedBy,
|
|
339
|
+
assert.equal(request.requestedBy, USER);
|
|
344
340
|
for (const forbidden of ["registry", "image", "digest"]) {
|
|
345
341
|
assert.ok(!(forbidden in request));
|
|
346
342
|
}
|
|
@@ -10,7 +10,6 @@ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
|
10
10
|
import type { Context } from "openapi-backend";
|
|
11
11
|
import { getSubFromEvent } from "../auth.js";
|
|
12
12
|
import type { OperationHandler, SystemOperationIds } from "../types.js";
|
|
13
|
-
import { requireInstanceOwner } from "./owner-guard.js";
|
|
14
13
|
|
|
15
14
|
class TargetVersionMismatchError extends HTTPError {
|
|
16
15
|
name = "TargetVersionMismatchError";
|
|
@@ -25,7 +24,17 @@ const controlDir = (): string =>
|
|
|
25
24
|
const manifestUrl = (): string | undefined =>
|
|
26
25
|
process.env.REMIT_UPDATE_MANIFEST_URL;
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The running version is a fact the updater owns: it reads the tag from `.env`
|
|
29
|
+
* and writes it into `state.json`, which is the only honest source across an
|
|
30
|
+
* update that rewrites that tag underneath a long-lived backend process. When no
|
|
31
|
+
* state file exists yet the backend has nothing authoritative to report, so it
|
|
32
|
+
* reports the version as unknown rather than fabricating one from its own
|
|
33
|
+
* process environment — a value captured at container start that drifts from the
|
|
34
|
+
* real running tag the moment an update lands. The first updater check writes a
|
|
35
|
+
* real `currentVersion` within seconds of the stack coming up.
|
|
36
|
+
*/
|
|
37
|
+
const UNKNOWN_VERSION = "unknown";
|
|
29
38
|
|
|
30
39
|
const notFound = (): APIGatewayProxyResult => ({
|
|
31
40
|
statusCode: 404,
|
|
@@ -33,11 +42,17 @@ const notFound = (): APIGatewayProxyResult => ({
|
|
|
33
42
|
body: JSON.stringify({ message: "Not found" }),
|
|
34
43
|
});
|
|
35
44
|
|
|
45
|
+
const unauthorized = (): APIGatewayProxyResult => ({
|
|
46
|
+
statusCode: 401,
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ message: "Unauthorized" }),
|
|
49
|
+
});
|
|
50
|
+
|
|
36
51
|
/**
|
|
37
52
|
* The self-update seam is off unless a manifest URL is configured (RFC 037 D8),
|
|
38
53
|
* which the hosted deployment leaves unset. Both endpoints answer 404 in that
|
|
39
|
-
* case
|
|
40
|
-
*
|
|
54
|
+
* case. Where it is on, any authenticated caller may use it — the account list
|
|
55
|
+
* is the trust boundary of a self-hosted instance.
|
|
41
56
|
*/
|
|
42
57
|
const guardManifestConfigured = (): APIGatewayProxyResult | null =>
|
|
43
58
|
manifestUrl() ? null : notFound();
|
|
@@ -60,7 +75,7 @@ const readState = (): SystemUpdateResponse | null => {
|
|
|
60
75
|
};
|
|
61
76
|
|
|
62
77
|
const emptyResource = (): SystemUpdateResponse => ({
|
|
63
|
-
currentVersion:
|
|
78
|
+
currentVersion: UNKNOWN_VERSION,
|
|
64
79
|
check: { status: "disabled" },
|
|
65
80
|
run: null,
|
|
66
81
|
});
|
|
@@ -110,7 +125,7 @@ const requestedResource = (
|
|
|
110
125
|
targetVersion: string,
|
|
111
126
|
requestedAt: string,
|
|
112
127
|
): SystemUpdateResponse => {
|
|
113
|
-
const from = state?.currentVersion ??
|
|
128
|
+
const from = state?.currentVersion ?? UNKNOWN_VERSION;
|
|
114
129
|
return {
|
|
115
130
|
currentVersion: from,
|
|
116
131
|
check: state?.check ?? { status: "disabled" },
|
|
@@ -140,8 +155,7 @@ export const SystemOperations: Record<
|
|
|
140
155
|
if (offSurface) return offSurface;
|
|
141
156
|
|
|
142
157
|
const event = args[0] as APIGatewayProxyEvent;
|
|
143
|
-
|
|
144
|
-
if (forbidden) return forbidden;
|
|
158
|
+
if (!getSubFromEvent(event)) return unauthorized();
|
|
145
159
|
|
|
146
160
|
return readState() ?? emptyResource();
|
|
147
161
|
},
|
|
@@ -154,8 +168,8 @@ export const SystemOperations: Record<
|
|
|
154
168
|
if (offSurface) return offSurface;
|
|
155
169
|
|
|
156
170
|
const event = args[0] as APIGatewayProxyEvent;
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
171
|
+
const sub = getSubFromEvent(event);
|
|
172
|
+
if (!sub) return unauthorized();
|
|
159
173
|
|
|
160
174
|
const { targetVersion } = context.request.requestBody as {
|
|
161
175
|
targetVersion: string;
|
|
@@ -178,7 +192,7 @@ export const SystemOperations: Record<
|
|
|
178
192
|
runId,
|
|
179
193
|
targetVersion,
|
|
180
194
|
requestedAt,
|
|
181
|
-
requestedBy:
|
|
195
|
+
requestedBy: sub,
|
|
182
196
|
});
|
|
183
197
|
|
|
184
198
|
return {
|
|
@@ -1,89 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
};
|