The standalone service in server/ was never deployed anywhere, and keeping it alongside a Vercel app would mean maintaining two implementations of the same validation rules. It is deleted; web/ is the only ingest path now. Its routes live at /v1/... rather than under /api, so the Android client's URL building is unchanged and CONSUMPTION_API_URL stays a bare origin. Credentials are split in two. INGEST_TOKEN is baked into the published APK and must be assumed extractable — anyone can unzip an APK and read its strings — so it opens the PUT endpoints and nothing else. Reading the history needs the dashboard password, which is never compiled into anything and reaches the browser as an HMAC-signed httpOnly cookie. The MongoDB URI stays a server-side environment variable; the car has never held one and still does not. The dashboard is a phone-shaped reader for the same data: totals for energy taken and paid, a daily energy chart, an efficiency trend, and every DC session with its power/SOC curve. It reuses the head unit's OLED palette so the two read as one app. Every page is a server component and the charts are hand-rolled SVG, so the whole thing renders without shipping client JavaScript. MongoClient is cached on globalThis: a fresh client per request means a fresh pool per request, and Atlas starts refusing connections long before the traffic would justify it. Index creation moved to the first write of each warm lambda, since a serverless deployment has no boot step to hang it on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
45 lines
2.1 KiB
TypeScript
45 lines
2.1 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
|
|
process.env.SESSION_SECRET = "0123456789abcdef0123456789abcdef";
|
|
process.env.DASHBOARD_PASSWORD = "correct-horse-battery";
|
|
process.env.INGEST_TOKEN = "a".repeat(64);
|
|
|
|
const { checkDashboardPassword, hasIngestToken, isValidSessionValue, issueSessionValue } =
|
|
await import("../src/lib/session.ts");
|
|
|
|
test("a freshly issued session validates and a tampered one does not", () => {
|
|
const value = issueSessionValue();
|
|
assert.equal(isValidSessionValue(value), true);
|
|
const [expiry, signature] = value.split(".");
|
|
// Moving the expiry out invalidates the signature, so a cookie cannot extend itself.
|
|
assert.equal(isValidSessionValue(`${Number(expiry) + 86_400}.${signature}`), false);
|
|
assert.equal(isValidSessionValue(`${expiry}.${"0".repeat(signature.length)}`), false);
|
|
assert.equal(isValidSessionValue(undefined), false);
|
|
assert.equal(isValidSessionValue("garbage"), false);
|
|
});
|
|
|
|
test("an expired session is refused even with a valid signature", async () => {
|
|
const past = Math.floor(Date.now() / 1000) - 10;
|
|
// Signed by the same secret, so only the embedded expiry rejects it.
|
|
const crypto = await import("node:crypto");
|
|
const signature = crypto.createHmac("sha256", process.env.SESSION_SECRET as string)
|
|
.update(String(past)).digest("hex");
|
|
assert.equal(isValidSessionValue(`${past}.${signature}`), false);
|
|
});
|
|
|
|
test("the dashboard password is exact", () => {
|
|
assert.equal(checkDashboardPassword("correct-horse-battery"), true);
|
|
assert.equal(checkDashboardPassword("correct-horse-batter"), false);
|
|
assert.equal(checkDashboardPassword(""), false);
|
|
});
|
|
|
|
test("the ingest token is only accepted as a bearer header", () => {
|
|
const token = "a".repeat(64);
|
|
const bearer = (value: string) => new Request("https://x/v1", { headers: { authorization: value } });
|
|
assert.equal(hasIngestToken(bearer(`Bearer ${token}`)), true);
|
|
assert.equal(hasIngestToken(bearer(token)), false);
|
|
assert.equal(hasIngestToken(bearer(`Bearer ${"b".repeat(64)}`)), false);
|
|
assert.equal(hasIngestToken(new Request("https://x/v1")), false);
|
|
});
|