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.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
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.5",
"version": "1.2.6",
"description": "Official Node.js SDK for source-timestamped OilPriceAPI energy data",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down
27 changes: 26 additions & 1 deletion scripts/validate-storefront-claims.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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`))];
}
Expand Down Expand Up @@ -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);

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.5";
export const SDK_VERSION = "1.2.6";

/**
* 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.5");
expect(packageJson.version).toBe("1.2.6");
expect(versionSource).toContain(`SDK_VERSION = "${packageJson.version}"`);
expect(firstRelease?.[1]).toBe(packageJson.version);
});
Expand Down
63 changes: 63 additions & 0 deletions tests/storefront-claims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down