From fc35ee034cb21c75d60cc79d97d6b861fe346e30 Mon Sep 17 00:00:00 2001 From: Karl Waldman Date: Wed, 12 Aug 2026 06:05:16 -0400 Subject: [PATCH] Scan every readable packed claim surface --- CHANGELOG.md | 9 ++++ package-lock.json | 4 +- package.json | 2 +- scripts/validate-storefront-claims.mjs | 27 ++++++++++- src/version.ts | 2 +- tests/release-readiness.test.ts | 2 +- tests/storefront-claims.test.ts | 63 ++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8fddf..34f2166 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.6] - 2026-08-12 + +### Fixed + +- Scan every customer-readable file in the exact packed npm distribution, + regardless of filename suffix, so future text, Markdown, source-map, or + extensionless package data cannot bypass the storefront claim contract. + Binary package data remains excluded from text parsing. + ## [1.2.5] - 2026-08-12 ### Fixed diff --git a/package-lock.json b/package-lock.json index 4898626..4b65ee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oilpriceapi", - "version": "1.2.5", + "version": "1.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oilpriceapi", - "version": "1.2.5", + "version": "1.2.6", "license": "MIT", "dependencies": { "ws": "^8.21.0" diff --git a/package.json b/package.json index 86d6985..7f9f5a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oilpriceapi", - "version": "1.2.5", + "version": "1.2.6", "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 5696206..1c6a335 100644 --- a/scripts/validate-storefront-claims.mjs +++ b/scripts/validate-storefront-claims.mjs @@ -57,6 +57,7 @@ const telemetryModifierGapWords = new Set([ ]); const maxStrongRewardSpan = 160; const maxTelemetryRewardSpan = 320; +const utf8Decoder = new TextDecoder("utf-8", { fatal: true }); function walkFiles(directory, extensions) { const files = []; @@ -71,6 +72,30 @@ function walkFiles(directory, extensions) { return files; } +function isReadableText(path) { + const contents = readFileSync(path); + if (contents.includes(0)) return false; + try { + utf8Decoder.decode(contents); + return true; + } catch { + return false; + } +} + +function walkReadableFiles(directory) { + const files = []; + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = resolve(directory, entry.name); + if (entry.isDirectory()) { + files.push(...walkReadableFiles(path)); + } else if (entry.isFile() && isReadableText(path)) { + files.push(path); + } + } + return files; +} + function matches(pattern, text) { return [...text.matchAll(new RegExp(pattern.source, `${pattern.flags}g`))]; } @@ -192,7 +217,7 @@ export function validateStorefront(baseRoot = defaultRoot) { export function validatePackage(packageRoot) { const files = [resolve(packageRoot, "README.md"), resolve(packageRoot, "package.json")]; - files.push(...walkFiles(resolve(packageRoot, "dist"), new Set([".js", ".ts", ".json"]))); + files.push(...walkReadableFiles(resolve(packageRoot, "dist"))); const failures = claimFailures(packageRoot, files); requireContractLink(packageRoot, failures); diff --git a/src/version.ts b/src/version.ts index 4f8af4c..be118e3 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.5"; +export const SDK_VERSION = "1.2.6"; /** * 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 6633886..bc55c69 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.5"); + expect(packageJson.version).toBe("1.2.6"); 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 0c6a75a..1e75425 100644 --- a/tests/storefront-claims.test.ts +++ b/tests/storefront-claims.test.ts @@ -85,6 +85,69 @@ describe("public storefront claims", () => { ); }); + it.each(["claim.txt", "claim.md", "claim.map", "NOTICE"])( + "rejects a telemetry quota reward in any future readable packed file: %s", + (filename) => { + const root = mkdtempSync(join(tmpdir(), "oilpriceapi-packed-readable-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", filename), + "Add optional telemetry headers (10% bonus for appUrl!).\n", + ); + + expect(validatePackage(root)).toContainEqual( + expect.stringContaining(`dist/resources/future/${filename}: telemetry quota reward`), + ); + }, + ); + + it("ignores binary package data without hiding adjacent readable claims", () => { + const root = mkdtempSync(join(tmpdir(), "oilpriceapi-packed-binary-data-")); + 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", "fixture.wasm"), + Buffer.from([0x00, 0x61, 0x73, 0x6d, 0xff, 0x00]), + ); + writeFileSync( + join(root, "dist", "resources", "future", "claim.txt"), + "Add optional telemetry headers (10% bonus for appUrl!).\n", + ); + + const failures = validatePackage(root); + expect(failures).not.toHaveLength(0); + expect(failures).toEqual( + expect.arrayContaining([ + expect.stringContaining("dist/resources/future/claim.txt: telemetry quota reward"), + ]), + ); + expect(failures.join("\n")).not.toContain("fixture.wasm"); + }); + + it("does not mistake a valid Unicode replacement character for binary data", () => { + const root = mkdtempSync(join(tmpdir(), "oilpriceapi-packed-unicode-text-")); + 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", "claim.txt"), + "Rendered replacement character: \uFFFD. Telemetry grants extra request credits.\n", + ); + + expect(validatePackage(root)).toContainEqual( + expect.stringContaining("dist/resources/future/claim.txt: 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);