forked from Mike/freno-dev
The analytics page could not answer "how many downloads, of which
product". Three of the four download doors recorded nothing: the tRPC
click proxy only left the generic request row, whose path is the raw URL
with the asset buried in `?batch=1&input={"asset_name":...}`, and the S3
file route that serves Sparkle updates and the Nook app logged nothing
at all. The one door that did log (the Nook redirect) kept a private copy
of the logVisit/enrich/flush sequence, under its own path.
All doors now go through trackDownload, which writes one row per event
under /api/downloads/<asset> and flushes it inside the request — a click
is low-volume and serverless is not guaranteed to run the 30s batch
timer after the response. The file route maps its prefix allowlist to
asset keys, so a version bump stays one asset rather than one per
release. getAnalyticsSummary groups those rows into a downloads block and
the freno.me panel renders it under the overview cards.
History is recovered to the degree it exists: the Nook door's rows and
the tRPC clicks still carry enough to derive from (asset name in the URL).
scripts/backfill-downloads.ts inserts those derived rows with a
`backfill-<source id>` primary key and INSERT OR IGNORE, leaving the
originals untouched, so re-running is a no-op. Run against the live table
it added 66 rows: gaze 42, thenook 19, inputhalo 4, lineage 1.
Tests: new backfill suite (derivation, visitor detail carried over,
idempotency, empty history) and summariseDownloads cases; the Nook route
test now pins the canonical path, and the NOOK-337 allowlist audit reads
the map keys instead of quoted strings. Red-checked by stubbing the
downloads block back to zeros — the summary case fails, then passes.
Full suite matches master.
Co-Authored-By: nw-flash <noreply@nw-flash.local>
MISC-48
49 lines
1.6 KiB
TypeScript
49 lines
1.6 KiB
TypeScript
/**
|
|
* Regression test for the downloads proxy filename allowlist (NOOK-337).
|
|
*
|
|
* The v0.10.0 publish failed its CI enclosure check because Sparkle names
|
|
* delta files after the app bundle ("Nook17-9.delta") while the DMG is named
|
|
* "TheNook-*.dmg" — and "Nook" was missing from the allowlist, so every Nook
|
|
* delta request 400'd with "Invalid file format" while the DMG passed.
|
|
*
|
|
* The route hits S3 and cannot be unit-tested under bun:test, so this is a
|
|
* STATIC SOURCE AUDIT — the same pattern the downloads.test.ts regressions
|
|
* use.
|
|
*/
|
|
import { describe, it, expect } from "bun:test";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const SOURCE = readFileSync(
|
|
join(import.meta.dir, "[filename].ts"),
|
|
"utf8"
|
|
);
|
|
|
|
/**
|
|
* The allowlist moved from an array to a `prefix → asset key` map when download
|
|
* tracking was added, so read the keys out of the map rather than matching
|
|
* quoted strings.
|
|
*/
|
|
const ALLOWLIST_KEYS = [
|
|
...(/const assetKeys: Record<string, string> = \{([^}]*)\}/.exec(SOURCE)?.[1] ?? "")
|
|
.matchAll(/"?([A-Za-z]+)"?\s*:/g)
|
|
].map((match) => match[1]);
|
|
|
|
describe("Downloads proxy filename allowlist (NOOK-337)", () => {
|
|
it("allows the Nook app-bundle prefix used by Sparkle delta names", () => {
|
|
expect(ALLOWLIST_KEYS).toContain("Nook");
|
|
});
|
|
|
|
it("still allows every previously served prefix", () => {
|
|
for (const prefix of ["Gaze", "InputHalo", "TheNook"]) {
|
|
expect(ALLOWLIST_KEYS).toContain(prefix);
|
|
}
|
|
});
|
|
|
|
it("still serves the extensions Sparkle fetches", () => {
|
|
for (const ext of [".dmg", ".delta", ".zip"]) {
|
|
expect(SOURCE).toContain(`"${ext}"`);
|
|
}
|
|
});
|
|
});
|