diff --git a/CHANGELOG.md b/CHANGELOG.md index 8386bd9..5d8fddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 6223119..4898626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oilpriceapi", - "version": "1.2.4", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oilpriceapi", - "version": "1.2.4", + "version": "1.2.5", "license": "MIT", "dependencies": { "ws": "^8.21.0" diff --git a/package.json b/package.json index 3416d79..86d6985 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/validate-storefront-claims.mjs b/scripts/validate-storefront-claims.mjs index 6a080fa..5696206 100644 --- a/scripts/validate-storefront-claims.mjs +++ b/scripts/validate-storefront-claims.mjs @@ -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, + ], ["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|(?]{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 [ @@ -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; } diff --git a/src/client.ts b/src/client.ts index b150e11..57e0022 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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; } diff --git a/src/types.ts b/src/types.ts index e8ccc5f..4a4217e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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) * @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; diff --git a/src/version.ts b/src/version.ts index 1780772..4f8af4c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -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 diff --git a/tests/release-readiness.test.ts b/tests/release-readiness.test.ts index 4044606..6633886 100644 --- a/tests/release-readiness.test.ts +++ b/tests/release-readiness.test.ts @@ -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); }); diff --git a/tests/storefront-claims.test.ts b/tests/storefront-claims.test.ts index 1747672..0c6a75a 100644 --- a/tests/storefront-claims.test.ts +++ b/tests/storefront-claims.test.ts @@ -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"); @@ -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( @@ -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); @@ -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")); }); });