Skip to content

Commit 25ea157

Browse files
committed
fix: address community registry cache review
1 parent 2188fb3 commit 25ea157

4 files changed

Lines changed: 69 additions & 6 deletions

File tree

src/lib/communityMeshes.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ export interface CommunityMeshesResponse {
2626
const schema = JSON.parse(readFileSync(SCHEMA_PATH, "utf8")) as AnySchema;
2727
const validate = new Ajv2020({ allErrors: true }).compile(schema);
2828

29+
const deepFreeze = <T>(value: T): T => {
30+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
31+
Object.freeze(value);
32+
for (const nested of Object.values(value as Record<string, unknown>)) {
33+
deepFreeze(nested);
34+
}
35+
}
36+
return value;
37+
};
38+
2939
const validPsk = (psk: unknown): boolean => {
3040
if (psk === null) return true;
3141
if (typeof psk !== "string") return false;
@@ -123,20 +133,22 @@ export const loadCommunityMeshes = (
123133
throw new Error(`duplicate community id: ${communities[index].id}`);
124134
}
125135
}
126-
return communities;
136+
return deepFreeze(communities);
127137
};
128138

129139
const communities = loadCommunityMeshes();
130-
const response: CommunityMeshesResponse = Object.freeze({
140+
const response: CommunityMeshesResponse = deepFreeze({
131141
apiVersion: "v1",
132142
schemaVersion: 1,
133143
generatedAt: new Date().toISOString(),
134144
sourceRevision: process.env.VERCEL_GIT_COMMIT_SHA ?? "local",
135145
communities,
136146
});
137147

148+
export const registryJson = JSON.stringify(response, null, 2);
149+
138150
export const registryEtag = `"${createHash("sha256")
139-
.update(JSON.stringify(communities))
151+
.update(registryJson)
140152
.digest("base64url")}"`;
141153

142154
export const getCommunityMeshes = (): CommunityMeshesResponse => response;

src/routes/communityMeshes.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
import type { App } from "@tinyhttp/app";
22
import {
33
getCommunityMesh,
4-
getCommunityMeshes,
54
getCommunityMeshSchema,
65
registryEtag,
6+
registryJson,
77
} from "../lib/communityMeshes.js";
88

9+
const ifNoneMatchMatches = (
10+
header: string | undefined,
11+
etag: string,
12+
): boolean => {
13+
if (!header) return false;
14+
const validators = header.match(/(?:W\/)?"[^"]*"|\*/g) ?? [];
15+
return validators.some(
16+
(validator) => validator === "*" || validator.replace(/^W\//, "") === etag,
17+
);
18+
};
19+
920
export const CommunityMeshRoutes = (app: App): void => {
1021
app.get("/v1/community-meshes", (req, res) => {
1122
res.setHeader("Cache-Control", "public, max-age=3600");
1223
res.setHeader("ETag", registryEtag);
13-
if (req.headers["if-none-match"] === registryEtag) {
24+
if (ifNoneMatchMatches(req.headers["if-none-match"], registryEtag)) {
1425
return res.status(304).end();
1526
}
16-
return res.json(getCommunityMeshes());
27+
res.setHeader("Content-Type", "application/json; charset=utf-8");
28+
return res.send(registryJson);
1729
});
1830

1931
app.get("/v1/community-meshes/schema", (_req, res) => {

tests/communityMeshes.routes.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { strict as assert } from "node:assert";
2+
import { createHash } from "node:crypto";
23
import test from "node:test";
34
import { App } from "@tinyhttp/app";
45
import { CommunityMeshRoutes } from "../src/routes/communityMeshes.js";
@@ -30,6 +31,11 @@ test("registry uses ETags for cache revalidation", async () => {
3031
const first = await fetch(`${origin}/v1/community-meshes`);
3132
const etag = first.headers.get("etag");
3233
assert.ok(etag);
34+
const body = await first.text();
35+
const expectedEtag = `"${createHash("sha256")
36+
.update(body)
37+
.digest("base64url")}"`;
38+
assert.equal(etag, expectedEtag);
3339

3440
const second = await fetch(`${origin}/v1/community-meshes`, {
3541
headers: { "If-None-Match": etag },
@@ -39,6 +45,25 @@ test("registry uses ETags for cache revalidation", async () => {
3945
});
4046
});
4147

48+
test("registry accepts standard If-None-Match validator forms", async () => {
49+
const app = new App();
50+
CommunityMeshRoutes(app);
51+
52+
await withServer(app, async (origin) => {
53+
const first = await fetch(`${origin}/v1/community-meshes`);
54+
const etag = first.headers.get("etag");
55+
assert.ok(etag);
56+
57+
const validators = [`W/${etag}`, `"unrelated", W/${etag}`, "*"];
58+
for (const validator of validators) {
59+
const response = await fetch(`${origin}/v1/community-meshes`, {
60+
headers: { "If-None-Match": validator },
61+
});
62+
assert.equal(response.status, 304, validator);
63+
}
64+
});
65+
});
66+
4267
test("serves the schema and returns 404 for an unknown community", async () => {
4368
const app = new App();
4469
CommunityMeshRoutes(app);

tests/communityMeshes.schema.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ test("accepts a public custom-modem profile with public MQTT", () => {
5252
assert.equal(validate(fixture("valid-public-custom.json")), true);
5353
});
5454

55+
test("loaded registry records are deeply immutable", () => {
56+
withRecords([fixture("valid-public-custom.json")], (directory) => {
57+
const communities = loadCommunityMeshes(directory);
58+
const community = communities[0];
59+
const coverage = community.coverage as { coordinates: number[][][] };
60+
61+
assert.equal(Object.isFrozen(communities), true);
62+
assert.equal(Object.isFrozen(community), true);
63+
assert.equal(Object.isFrozen(coverage), true);
64+
assert.equal(Object.isFrozen(coverage.coordinates), true);
65+
assert.equal(Object.isFrozen(coverage.coordinates[0][0]), true);
66+
});
67+
});
68+
5569
test("rejects a licensed profile with an encrypted channel", () => {
5670
assert.equal(validate(fixture("invalid-licensed-psk.json")), false);
5771
});

0 commit comments

Comments
 (0)