Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backends/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ForbiddenError } from "../errors.js";
import type { PecansReleases } from "../models/index.js";
import type { PecansAssetDTO } from "../models/PecansAsset.js";

const DEFAULT_CACHE_MAX_AGE = 60 * 60 * 2; // 2 hours in seconds
export const DEFAULT_CACHE_MAX_AGE = 60 * 60 * 2; // 2 hours in seconds

export interface BackendOpts {
refreshSecret?: string;
Expand Down
22 changes: 18 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import express, {
type Response,
} from "express";
import { errorHandler } from "./errors.js";
import { PecansGitHubBackend } from "./backends/index.js";
import {
DEFAULT_CACHE_MAX_AGE,
PecansGitHubBackend,
} from "./backends/index.js";
import { Pecans, type PecansOptions } from "./pecans.js";

export * from "./backends/index.js";
Expand All @@ -15,12 +18,23 @@ export * from "./pecans.js";
export * from "./service.js";
export * from "./utils/index.js";

/**
* Parse the PECANS_CACHE_MAX_AGE env var (seconds). Anything but a
* non-negative integer falls back to the backend's default: a NaN would
* flow into the backend's cache-age comparison, where every check comes
* out false and the cache is never refreshed after the initial fetch,
* while parseInt-style permissive parsing would silently accept values
* like "3600ms" or "-1" (a negative age refreshes on every request).
*/
export function parseCacheMaxAge(value: string | undefined): number {
if (!value || !/^\d+$/.test(value)) return DEFAULT_CACHE_MAX_AGE;
return Number.parseInt(value, 10);
}

export function configure() {
const PECANS_BACKEND = process.env.PECANS_BACKEND || "PecansGithubBackend";
const basePath = process.env.PECANS_BASE_PATH || "";
const cacheMaxAge = process.env.PECANS_CACHE_MAX_AGE
? parseInt(process.env.PECANS_CACHE_MAX_AGE)
: 60 * 60 * 2; // Default 2 hours
const cacheMaxAge = parseCacheMaxAge(process.env.PECANS_CACHE_MAX_AGE);
// enables POST /webhook/refresh (see README); without it the cache-bust
// endpoint stays disabled
const refreshSecret = process.env.PECANS_REFRESH_SECRET;
Expand Down
32 changes: 31 additions & 1 deletion test/unit/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { PecansGitHubBackend } from "../../src/backends/index.js";
import { configure, parseTrustProxy } from "../../src/index.js";
import {
configure,
parseCacheMaxAge,
parseTrustProxy,
} from "../../src/index.js";

describe("Index", () => {
let originalEnv: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -106,6 +110,32 @@ describe("Index", () => {
});
});

describe("parseCacheMaxAge", () => {
it("parses numeric values as seconds", () => {
expect(parseCacheMaxAge("3600")).toBe(3600);
expect(parseCacheMaxAge("0")).toBe(0);
});

it("falls back to the 2 hour default when unset", () => {
expect(parseCacheMaxAge(undefined)).toBe(7200);
expect(parseCacheMaxAge("")).toBe(7200);
});

it("falls back to the 2 hour default for non-numeric values", () => {
// NaN would disable cache refreshes entirely: the backend's
// cacheAge > cacheMaxAgeMs check is always false against NaN
expect(parseCacheMaxAge("garbage")).toBe(7200);
});
Comment thread
dopry marked this conversation as resolved.

it("falls back for partially-numeric and negative values", () => {
// parseInt would read "3600ms" as 3600 and "-1" as -1; a negative
// age makes the cache look expired on every request
expect(parseCacheMaxAge("3600ms")).toBe(7200);
expect(parseCacheMaxAge("-1")).toBe(7200);
expect(parseCacheMaxAge("1.5")).toBe(7200);
});
});

describe("parseTrustProxy", () => {
it("parses JSON scalars and arrays", () => {
expect(parseTrustProxy("true")).toBe(true);
Expand Down