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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.5] - 2026-08-12

### Fixed

- Remove the nonexistent telemetry request-bonus claim from authored and
generated package surfaces. The recursive storefront guard now rejects
equivalent telemetry or app-metadata quota rewards in both source and the
exact packed npm artifact.

## [1.2.4] - 2026-08-11

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "oilpriceapi",
"version": "1.2.4",
"version": "1.2.5",
"description": "Official Node.js SDK for source-timestamped OilPriceAPI energy data",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down
108 changes: 104 additions & 4 deletions scripts/validate-storefront-claims.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,49 @@ const blocked = [
["price comparison", /\bbloomberg\b|\b\d+(?:\.\d+)?%\s+less\s+cost\b/i],
["unreviewed plan name", /\bprofessional\+?\b|\bstarter plan\b|\bscale tier\b/i],
["unreviewed plan price", /\$\d+(?:\.\d+)?\s*(?:\/|per\s+)(?:mo(?:nth)?|year)\b/i],
["fixed allowance", /\b(?:1,000|100)\s+requests?(?:\/month|\s+per month|\s+\(lifetime\))/i],
[
"fixed allowance",
/\b(?:1,000|100)\s+requests?(?:\/month|\s+per month|\s+\(lifetime\))/i,
"quota promise",
/\bdoes\s+not\s+consume.{0,40}\bquota\b|\bunlimited\s+(?:history|webhooks?|requests?|commodit)/i,
],
["quota promise", /\bdoes\s+not\s+consume.{0,40}\bquota\b|\bunlimited\s+(?:history|webhooks?|requests?|commodit)/i],
[
"fixed quota window",
/\b(?:daily|weekly|monthly|yearly)\s+(?:(?:api|request)\s+)?quota\b|\b(?:(?:api|request)\s+)?quota\b.{0,40}\b(?:daily|weekly|monthly|yearly)\b/i,
],
["free-tier claim", /\bfree\s+tier\b|\bfree\s+api\s+key\b/i],
["free endpoint claim", /\b(?:endpoint|resource|api)\s+is\s+free\b|\bincluded\s+in\s+all\s+tiers\b/i],
[
"free endpoint claim",
/\b(?:endpoint|resource|api)\s+is\s+free\b|\bincluded\s+in\s+all\s+tiers\b/i,
],
Comment thread
karlwaldman marked this conversation as resolved.
["fixed query allowance", /\b\d[\d,]*\s+(?:station\s+)?queries?\s*(?:\/|per\s+)month\b/i],
[
"fixed demo rate",
/\b\d+\s+(?:requests?|reqs?\.?)\s*(?:(?:per|an?)\s+|\/\s*)(?:minutes?|mins?|hours?|hrs?|days?)\b/i,
],
];
const telemetryIdentity =
/\b(?:telemetry|app(?:lication)?[- ]+(?:metadata|url|name)|app[_ -]?url|app[_ -]?name|x-app-(?:url|name))\b/i;
const telemetryStrongReward =
/\b(?:bonus|increase(?:s|d)?|unlock(?:s|ed)?|earn(?:s|ed)?|grant(?:s|ed)?|reward(?:s|ed)?|boost(?:s|ed)?)\b/i;
const telemetryModifierReward = /\b(?:more|extra|additional)\b/i;
const telemetryQuotaSignal =
/\b(?:api[- ]+)?(?:requests?|calls?|quota|limits?|allowances?|credits?)\b|(?<![\w.])\d+(?:\.\d+)?\s*%/i;
const telemetryBoundary = /(?:\r?\n)+|[!?;]+(?:\s+|$)|\.(?:\s+|$)/g;
const telemetryModifierGapWords = new Set([
"account",
"annual",
"api",
"call",
"daily",
"hourly",
"monthly",
"quota",
"rate",
"request",
"usage",
]);
const maxStrongRewardSpan = 160;
const maxTelemetryRewardSpan = 320;

function walkFiles(directory, extensions) {
const files = [];
Expand All @@ -45,6 +71,77 @@ function walkFiles(directory, extensions) {
return files;
}

function matches(pattern, text) {
return [...text.matchAll(new RegExp(pattern.source, `${pattern.flags}g`))];
}

function boundedSegments(text) {
const segments = [];
let start = 0;
for (const boundary of text.matchAll(telemetryBoundary)) {
if (text.slice(start, boundary.index).trim()) {
segments.push({ offset: start, text: text.slice(start, boundary.index) });
}
start = boundary.index + boundary[0].length;
}
if (text.slice(start).trim()) {
segments.push({ offset: start, text: text.slice(start) });
}
return segments;
}

function telemetryRewardClaims(text) {
const claims = [];
const seen = new Set();
for (const segment of boundedSegments(text)) {
const searchable = segment.text.replace(/<[^>]{1,500}>/g, " ");
const identities = matches(telemetryIdentity, searchable);
const quotaSignals = matches(telemetryQuotaSignal, searchable);
const strongRewards = matches(telemetryStrongReward, searchable);
const modifierRewards = matches(telemetryModifierReward, searchable);
const rewardPairs = [];

for (const reward of strongRewards) {
for (const quota of quotaSignals) {
const start = Math.min(reward.index, quota.index);
const end = Math.max(reward.index + reward[0].length, quota.index + quota[0].length);
if (end - start <= maxStrongRewardSpan) rewardPairs.push({ start, end });
}
}
for (const reward of modifierRewards) {
for (const quota of quotaSignals) {
if (reward.index + reward[0].length > quota.index) continue;
const gap = searchable.slice(reward.index + reward[0].length, quota.index);
const words = gap.toLowerCase().match(/[a-z]+/g) ?? [];
if (gap.length <= 48 && words.every((word) => telemetryModifierGapWords.has(word))) {
rewardPairs.push({
start: reward.index,
end: quota.index + quota[0].length,
});
}
}
}

for (const identity of identities) {
const related = rewardPairs
.map((pair) => ({
start: Math.min(identity.index, pair.start),
end: Math.max(identity.index + identity[0].length, pair.end),
}))
.filter((span) => span.end - span.start <= maxTelemetryRewardSpan)
.sort((left, right) => left.end - left.start - (right.end - right.start))[0];
if (!related) continue;
const claim = searchable.slice(related.start, related.end).replace(/\s+/g, " ").trim();
const key = `${segment.offset + related.start}:${claim}`;
if (!seen.has(key)) {
seen.add(key);
claims.push(claim);
}
}
}
return claims;
}

export function discoverStorefrontSurfaces(baseRoot = defaultRoot) {
const files = [resolve(baseRoot, "README.md"), resolve(baseRoot, "package.json")];
for (const [directory, extensions] of [
Expand All @@ -66,6 +163,9 @@ function claimFailures(baseRoot, files) {
failures.push(`${relative(baseRoot, path)}: ${label} ${JSON.stringify(match[0])}`);
}
}
for (const claim of telemetryRewardClaims(contents)) {
failures.push(`${relative(baseRoot, path)}: telemetry quota reward ${JSON.stringify(claim)}`);
}
}
return failures;
}
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ export class OilPriceAPI {
"X-SDK-Version": SDK_VERSION,
};

// Add optional telemetry headers (10% bonus for appUrl!)
// Add optional usage-attribution headers.
if (this.appUrl) {
headers["X-App-URL"] = this.appUrl;
}
Expand Down
6 changes: 2 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,13 @@ export interface OilPriceAPIConfig {
debug?: boolean;

/**
* Your application's URL (optional, for telemetry)
* Helps us understand how the API is being used and may unlock
* a 10% bonus to your request limit.
* Your application's URL (optional, for usage attribution)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
* @example "https://myapp.com"
*/
appUrl?: string;

/**
* Your application's name (optional, for telemetry)
* Your application's name (optional, for usage attribution)
* @example "MyFuelPriceTracker"
*/
appName?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* - X-Client-Version header
* - Package.json (should match)
*/
export const SDK_VERSION = "1.2.4";
export const SDK_VERSION = "1.2.5";

/**
* SDK identifier used in User-Agent and X-Api-Client headers
Expand Down
2 changes: 1 addition & 1 deletion tests/release-readiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe("release readiness", () => {
const changelog = read("CHANGELOG.md");
const firstRelease = changelog.match(/^## \[([^\]]+)\]/m);

expect(packageJson.version).toBe("1.2.4");
expect(packageJson.version).toBe("1.2.5");
expect(versionSource).toContain(`SDK_VERSION = "${packageJson.version}"`);
expect(firstRelease?.[1]).toBe(packageJson.version);
});
Expand Down
68 changes: 58 additions & 10 deletions tests/storefront-claims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ describe("public storefront claims", () => {
});

it("discovers generated docs and nested package source", () => {
const surfaces = discoverStorefrontSurfaces().map((path) =>
relative(process.cwd(), path),
);
const surfaces = discoverStorefrontSurfaces().map((path) => relative(process.cwd(), path));

expect(surfaces).toContain("docs/index.html");
expect(surfaces).toContain("src/index.ts");
Expand All @@ -36,10 +34,7 @@ describe("public storefront claims", () => {
const root = mkdtempSync(join(tmpdir(), "oilpriceapi-package-claims-"));
scratch.push(root);
mkdirSync(join(root, "dist", "resources"), { recursive: true });
writeFileSync(
join(root, "README.md"),
"https://api.oilpriceapi.com/product-facts.json\n",
);
writeFileSync(join(root, "README.md"), "https://api.oilpriceapi.com/product-facts.json\n");
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "9.9.9" }));
writeFileSync(join(root, "dist", "version.js"), 'export const SDK_VERSION = "9.9.9";\n');
writeFileSync(
Expand All @@ -52,6 +47,61 @@ describe("public storefront claims", () => {
);
});

it("rejects telemetry quota rewards in future nested authored source", () => {
const root = mkdtempSync(join(tmpdir(), "oilpriceapi-authored-telemetry-claim-"));
scratch.push(root);
mkdirSync(join(root, "docs"), { recursive: true });
mkdirSync(join(root, "src", "resources", "future"), { recursive: true });
writeFileSync(join(root, "README.md"), "https://api.oilpriceapi.com/product-facts.json\n");
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "9.9.9" }));
writeFileSync(join(root, "src", "version.ts"), 'export const SDK_VERSION = "9.9.9";\n');
writeFileSync(
join(root, "src", "resources", "future", "client.ts"),
"/** Telemetry metadata unlocks additional API calls for your app. */\n",
);

expect(validateStorefront(root)).toContainEqual(
expect.stringContaining("src/resources/future/client.ts: telemetry quota reward"),
);
});

it.each([
"App telemetry may unlock a 10% bonus to your request limit.",
"10% bonus for appUrl API calls.",
"X-App-URL earns extra request credits.",
"More requests are granted when application metadata is sent.",
"Sending appUrl increases your quota allowance.",
])("rejects a telemetry quota reward in a future packed declaration: %s", (claim) => {
const root = mkdtempSync(join(tmpdir(), "oilpriceapi-packed-telemetry-claim-"));
scratch.push(root);
mkdirSync(join(root, "dist", "resources", "future"), { recursive: true });
writeFileSync(join(root, "README.md"), "https://api.oilpriceapi.com/product-facts.json\n");
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "9.9.9" }));
writeFileSync(join(root, "dist", "version.js"), 'export const SDK_VERSION = "9.9.9";\n');
writeFileSync(join(root, "dist", "resources", "future", "client.d.ts"), `/** ${claim} */\n`);

expect(validatePackage(root)).toContainEqual(
expect.stringContaining("dist/resources/future/client.d.ts: telemetry quota reward"),
);
});

it("does not reject telemetry attribution without a quota reward", () => {
const root = mkdtempSync(join(tmpdir(), "oilpriceapi-packed-telemetry-attribution-"));
scratch.push(root);
mkdirSync(join(root, "dist"), { recursive: true });
writeFileSync(join(root, "README.md"), "https://api.oilpriceapi.com/product-facts.json\n");
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "9.9.9" }));
writeFileSync(join(root, "dist", "version.js"), 'export const SDK_VERSION = "9.9.9";\n');
writeFileSync(
join(root, "dist", "client.d.ts"),
"/** Optional app metadata identifies SDK usage. Entitlements come from Product Facts.\n" +
" * Telemetry sends extra application metadata with API requests.\n" +
" */\n",
);

expect(validatePackage(root)).toEqual([]);
});

it("rejects a fixed quota window without requiring a numeric allowance", () => {
const root = mkdtempSync(join(tmpdir(), "oilpriceapi-package-quota-"));
scratch.push(root);
Expand All @@ -63,8 +113,6 @@ describe("public storefront claims", () => {
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "9.9.9" }));
writeFileSync(join(root, "dist", "version.js"), 'export const SDK_VERSION = "9.9.9";\n');

expect(validatePackage(root)).toContainEqual(
expect.stringContaining("fixed quota window"),
);
expect(validatePackage(root)).toContainEqual(expect.stringContaining("fixed quota window"));
});
});